From f2cb62c74977317e606a292805205464cdaa5729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Sat, 11 Aug 2012 20:04:55 +0100 Subject: [PATCH 01/13] Implemented an init.d script to manage the Jungle. The script allows running Puma apps as daemons using start-stop-daemon and adds an easy way to log its activity. --- README.md | 4 + tools/jungle/README.md | 52 +++++++ tools/jungle/puma | 332 +++++++++++++++++++++++++++++++++++++++++ tools/jungle/run-puma | 3 + 4 files changed, 391 insertions(+) create mode 100644 tools/jungle/README.md create mode 100755 tools/jungle/puma create mode 100755 tools/jungle/run-puma diff --git a/README.md b/README.md index 583acb4b..c8a2a72c 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ If you start puma with `-S some/path` then you can pass that same path to the `p will cause the server to perform a restart. `pumactl` is a simple CLI frontend to the control/status app described above. +## Managing multiple Pumas / init.d script + +If you want an easy way to manage multiple scripts at once check "tools/jungle" for an init.d script. + ## License Puma is copyright 2011 Evan Phoenix and contributors. It is licensed under the BSD license. See the include LICENSE file for details. diff --git a/tools/jungle/README.md b/tools/jungle/README.md new file mode 100644 index 00000000..5a250839 --- /dev/null +++ b/tools/jungle/README.md @@ -0,0 +1,52 @@ +# Puma daemon service + +Init script to manage multiple Puma servers on the same box using start-stop-daemon. + +## Installation + + # Copy the init script to services directory + sudo cp puma /etc/init.d + sudo chmod +x /etc/init.d/puma + + # Make it start at boot time. + sudo update-rc.d -f puma defaults + + # Copy the Puma runner to an accessible location + sudo cp run-puma /usr/local/bin + sudo chmod +x /usr/local/bin/run-puma + + # Create an empty configuration file + sudo touch /etc/puma.conf + +## Managing the jungle + +Puma apps are held in /etc/puma.conf by default. It's mainly a CSV file and every line represents one app. Here's the syntax: + + app-path,user,config-file-path,log-file-path + +You can add an instance by editing the file or running the following command: + + sudo /etc/init.d/puma add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log + +The config and log paths are optional parameters and default to: + +* config: /path/to/app/*config/puma.rb* +* log: /path/to/app/*config/puma.log* + +To remove an app, simply delete the line from the config file or run: + + sudo /etc/init.d/puma remove /path/to/app + +The command will make sure the Puma instance stops before removing it from the jungle. + +## Assumptions + +* The script expects a temporary folder named /path/to/app/*tmp/puma* to exist. Create it if it's not there by default. +The pid and state files should live there and must be called: *tmp/puma/pid* and *tmp/puma/state*. +You can change those if you want but you'll have to adapt the script for it to work. + +* Here's what a minimal app's config file should have: + + pidfile "/path/to/app/tmp/puma/pid" + state_path "/path/to/app/tmp/puma/state" + activate_control_app diff --git a/tools/jungle/puma b/tools/jungle/puma new file mode 100755 index 00000000..8bcee586 --- /dev/null +++ b/tools/jungle/puma @@ -0,0 +1,332 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: puma +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Example initscript +# Description: This file should be used to construct scripts to be +# placed in /etc/init.d. +### END INIT INFO + +# Author: Darío Javier Cravero +# +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/usr/local/bin:/usr/local/sbin/:/sbin:/usr/sbin:/bin:/usr/bin +DESC="Puma rack web server" +NAME=puma +DAEMON=$NAME +SCRIPTNAME=/etc/init.d/$NAME +CONFIG=/etc/puma.conf +JUNGLE=`cat $CONFIG` +RUNPUMA=/usr/local/bin/run-puma + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the jungle +# +do_start() { + log_daemon_msg "=> Running the jungle..." + for i in $JUNGLE; do + dir=`echo $i | cut -d , -f 1` + user=`echo $i | cut -d , -f 2` + config_file=`echo $i | cut -d , -f 3` + if [ "$config_file" = "" ]; then + config_file="$dir/config/puma.rb" + fi + log_file=`echo $i | cut -d , -f 4` + if [ "$log_file" = "" ]; then + log_file="$dir/log/puma.log" + fi + do_start_one $dir $user $config_file $log_file + done +} + +do_start_one() { + PIDFILE=$1/tmp/puma/pid + if [ -e $PIDFILE ]; then + PID=`cat $PIDFILE` + # If the puma isn't running, run it, otherwise restart it. + if [ "`ps -A -o pid= | grep -c $PID`" -eq 0 ]; then + do_start_one_do $1 $2 $3 $4 + else + do_restart_one $1 + fi + else + do_start_one_do $1 $2 $3 $4 + fi +} + +do_start_one_do() { + log_daemon_msg "--> Woke up puma $1" + log_daemon_msg "user $2" + log_daemon_msg "log to $4" + start-stop-daemon --verbose --start --chdir $1 --chuid $2 --background --exec $RUNPUMA -- $1 $3 $4 +} + +# +# Function that stops the jungle +# +do_stop() { + log_daemon_msg "=> Putting all the beasts to bed..." + for i in $JUNGLE; do + dir=`echo $i | cut -d , -f 1` + do_stop_one $dir + done +} +# +# Function that stops the daemon/service +# +do_stop_one() { + log_daemon_msg "--> Stopping $1" + PIDFILE=$1/tmp/puma/pid + STATEFILE=$1/tmp/puma/state + if [ -e $PIDFILE ]; then + PID=`cat $PIDFILE` + if [ "`ps -A -o pid= | grep -c $PID`" -eq 0 ]; then + log_daemon_msg "---> Puma $1 isn't running." + else + log_daemon_msg "---> About to kill PID `cat $PIDFILE`" + pumactl --state $STATEFILE stop + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE $STATEFILE + fi + else + log_daemon_msg "---> No puma here..." + fi + return 0 +} + +# +# Function that restarts the jungle +# +do_restart() { + for i in $JUNGLE; do + dir=`echo $i | cut -d , -f 1` + do_restart_one $dir + done +} + +# +# Function that sends a SIGUSR2 to the daemon/service +# +do_restart_one() { + PIDFILE=$1/tmp/puma/pid + i=`grep $1 $CONFIG` + dir=`echo $i | cut -d , -f 1` + + if [ -e $PIDFILE ]; then + log_daemon_msg "--> About to restart puma $1" + pumactl --state $dir/tmp/puma/state restart + # kill -s USR2 `cat $PIDFILE` + # TODO Check if process exist + else + log_daemon_msg "--> Your puma was never playing... Let's get it out there first" + user=`echo $i | cut -d , -f 2` + config_file=`echo $i | cut -d , -f 3` + if [ "$config_file" = "" ]; then + config_file="$dir/config/puma.rb" + fi + log_file=`echo $i | cut -d , -f 4` + if [ "$log_file" = "" ]; then + log_file="$dir/log/puma.log" + fi + do_start_one $dir $user $config_file $log_file + fi + return 0 +} + +# +# Function that statuss the jungle +# +do_status() { + for i in $JUNGLE; do + dir=`echo $i | cut -d , -f 1` + do_status_one $dir + done +} + +# +# Function that sends a SIGUSR2 to the daemon/service +# +do_status_one() { + PIDFILE=$1/tmp/puma/pid + i=`grep $1 $CONFIG` + dir=`echo $i | cut -d , -f 1` + + if [ -e $PIDFILE ]; then + log_daemon_msg "--> About to status puma $1" + pumactl --state $dir/tmp/puma/state stats + # kill -s USR2 `cat $PIDFILE` + # TODO Check if process exist + else + log_daemon_msg "--> $1 isn't there :(..." + fi + return 0 +} + +do_add() { + str="" + # App's directory + if [ -d "$1" ]; then + if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then + str=$1 + else + echo "The app is already being managed. Remove it if you want to update its config." + exit 1 + fi + else + echo "The directory $1 doesn't exist." + exit 1 + fi + # User to run it as + if [ "`grep -c "^$2:" /etc/passwd`" -eq 0 ]; then + echo "The user $2 doesn't exist." + exit 1 + else + str="$str,$2" + fi + # Config file + if [ "$3" != "" ]; then + if [ -e $3 ]; then + str="$str,$3" + else + echo "The config file $3 doesn't exist." + exit 1 + fi + fi + # Log file + if [ "$4" != "" ]; then + str="$str,$4" + fi + + # Add it to the jungle + echo $str >> $CONFIG + log_daemon_msg "Added a Puma to the jungle: $str. You still have to start it though." +} + +do_remove() { + if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then + echo "There's no app $1 to remove." + else + # Stop it first. + do_stop_one $1 + # Remove it from the config. + sed -i "\\:^$1:d" $CONFIG + log_daemon_msg "Removed a Puma from the jungle: $1." + fi +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + if [ "$#" -eq 1 ]; then + do_start + else + i=`grep $2 $CONFIG` + dir=`echo $i | cut -d , -f 1` + user=`echo $i | cut -d , -f 2` + config_file=`echo $i | cut -d , -f 3` + if [ "$config_file" = "" ]; then + config_file="$dir/config/puma.rb" + fi + log_file=`echo $i | cut -d , -f 4` + if [ "$log_file" = "" ]; then + log_file="$dir/log/puma.log" + fi + do_start_one $dir $user $config_file $log_file + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + if [ "$#" -eq 1 ]; then + do_stop + else + i=`grep $2 $CONFIG` + dir=`echo $i | cut -d , -f 1` + do_stop_one $dir + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + # TODO Implement. + log_daemon_msg "Status $DESC" "$NAME" + if [ "$#" -eq 1 ]; then + do_status + else + i=`grep $2 $CONFIG` + dir=`echo $i | cut -d , -f 1` + do_status_one $dir + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + restart) + log_daemon_msg "Restarting $DESC" "$NAME" + if [ "$#" -eq 1 ]; then + do_restart + else + i=`grep $2 $CONFIG` + dir=`echo $i | cut -d , -f 1` + do_restart_one $dir + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + add) + if [ "$#" -lt 3 ]; then + echo "Please, specifiy the app's directory and the user that will run it at least." + echo " Usage: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log" + echo " config and log are optionals." + exit 1 + else + do_add $2 $3 $4 $5 + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + remove) + if [ "$#" -lt 2 ]; then + echo "Please, specifiy the app's directory to remove." + exit 1 + else + do_remove $2 + fi + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + *) + echo "Usage:" >&2 + echo " Run the jungle: $SCRIPTNAME {start|stop|status|restart}" >&2 + echo " Add a Puma: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log" + echo " config and log are optionals." + echo " Remove a Puma: $SCRIPTNAME remove /path/to/app" + echo " On a Puma: $SCRIPTNAME {start|stop|status|restart} PUMA-NAME" >&2 + exit 3 + ;; +esac +: diff --git a/tools/jungle/run-puma b/tools/jungle/run-puma new file mode 100755 index 00000000..059d177e --- /dev/null +++ b/tools/jungle/run-puma @@ -0,0 +1,3 @@ +#!/bin/bash +app=$1; config=$2; log=$3; +cd $app && exec bundle exec puma -C $config 2>&1 >> $log From 6a458da4100bbeaa9b0dbf023bb0c99c3fda53a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Sat, 11 Aug 2012 21:10:04 +0200 Subject: [PATCH 02/13] Update tools/jungle/README.md --- tools/jungle/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/jungle/README.md b/tools/jungle/README.md index 5a250839..f8fe6a91 100644 --- a/tools/jungle/README.md +++ b/tools/jungle/README.md @@ -47,6 +47,8 @@ You can change those if you want but you'll have to adapt the script for it to w * Here's what a minimal app's config file should have: - pidfile "/path/to/app/tmp/puma/pid" - state_path "/path/to/app/tmp/puma/state" - activate_control_app +``` +pidfile "/path/to/app/tmp/puma/pid" +state_path "/path/to/app/tmp/puma/state" +activate_control_app +``` From 43327bea6c55ce016511301cf3e6a03a216ed884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Sun, 12 Aug 2012 13:21:38 +0200 Subject: [PATCH 03/13] Update README.md Now that it made it into the official repo, I'm linking the jungle from the main readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8a2a72c..5f4baf9e 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ will cause the server to perform a restart. `pumactl` is a simple CLI frontend t ## Managing multiple Pumas / init.d script -If you want an easy way to manage multiple scripts at once check "tools/jungle" for an init.d script. +If you want an easy way to manage multiple scripts at once check [tools/jungle](https://github.com/puma/puma/tree/master/tools/jungle) for an init.d script. ## License From f50acd28f9947a365a0c68d3a46b144976b9e109 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Sun, 12 Aug 2012 08:11:00 -0700 Subject: [PATCH 04/13] Bump to 1.6.1 --- History.txt | 5 +++++ Manifest.txt | 3 ++- lib/puma/const.rb | 2 +- puma.gemspec | 8 ++++---- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/History.txt b/History.txt index aefb0dc6..816f4d79 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,8 @@ +=== 1.6.1 / 2012-07-23 + +* 1 packaging bug fixed: + * Include missing files + === 1.6.0 / 2012-07-23 * 1 major bug fix: diff --git a/Manifest.txt b/Manifest.txt index f54052a0..a143b867 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -1,6 +1,6 @@ -.travis.yml COPYING Gemfile +Gemfile.lock History.txt LICENSE Manifest.txt @@ -38,6 +38,7 @@ lib/puma/compat.rb lib/puma/configuration.rb lib/puma/const.rb lib/puma/control_cli.rb +lib/puma/detect.rb lib/puma/events.rb lib/puma/jruby_restart.rb lib/puma/null_io.rb diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 0ff153ac..c178c734 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -25,7 +25,7 @@ module Puma # too taxing on performance. module Const - PUMA_VERSION = VERSION = "1.6.0".freeze + PUMA_VERSION = VERSION = "1.6.1".freeze # The default number of seconds for another request within a persistent # session. diff --git a/puma.gemspec b/puma.gemspec index ecce23f9..a103fc77 100644 --- a/puma.gemspec +++ b/puma.gemspec @@ -2,23 +2,23 @@ Gem::Specification.new do |s| s.name = "puma" - s.version = "1.5.0" + s.version = "1.6.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Evan Phoenix"] - s.date = "2012-07-23" + s.date = "2012-08-12" s.description = "Puma is a simple, fast, and highly concurrent HTTP 1.1 server for Ruby web applications. It can be used with any application that supports Rack, and is considered the replacement for Webrick and Mongrel. It was designed to be the go-to server for [Rubinius](http://rubini.us), but also works well with JRuby and MRI. Puma is intended for use in both development and production environments.\n\nUnder the hood, Puma processes requests using a C-optimized Ragel extension (inherited from Mongrel) that provides fast, accurate HTTP 1.1 protocol parsing in a portable way. Puma then serves the request in a thread from an internal thread pool (which you can control). This allows Puma to provide real concurrency for your web application!\n\nWith Rubinius 2.0, Puma will utilize all cores on your CPU with real threads, meaning you won't have to spawn multiple processes to increase throughput. You can expect to see a similar benefit from JRuby.\n\nOn MRI, there is a Global Interpreter Lock (GIL) that ensures only one thread can be run at a time. But if you're doing a lot of blocking IO (such as HTTP calls to external APIs like Twitter), Puma still improves MRI's throughput by allowing blocking IO to be run concurrently (EventMachine-based servers such as Thin turn off this ability, requiring you to use special libraries). Your mileage may vary. In order to get the best throughput, it is highly recommended that you use a Ruby implementation with real threads like [Rubinius](http://rubini.us) or [JRuby](http://jruby.org)." s.email = ["evan@phx.io"] s.executables = ["puma", "pumactl"] s.extensions = ["ext/puma_http11/extconf.rb"] s.extra_rdoc_files = ["History.txt", "Manifest.txt"] - s.files = [".travis.yml", "COPYING", "Gemfile", "History.txt", "LICENSE", "Manifest.txt", "README.md", "Rakefile", "TODO", "bin/puma", "bin/pumactl", "examples/CA/cacert.pem", "examples/CA/newcerts/cert_1.pem", "examples/CA/newcerts/cert_2.pem", "examples/CA/private/cakeypair.pem", "examples/CA/serial", "examples/config.rb", "examples/puma/cert_puma.pem", "examples/puma/csr_puma.pem", "examples/puma/puma_keypair.pem", "examples/qc_config.rb", "ext/puma_http11/PumaHttp11Service.java", "ext/puma_http11/ext_help.h", "ext/puma_http11/extconf.rb", "ext/puma_http11/http11_parser.c", "ext/puma_http11/http11_parser.h", "ext/puma_http11/http11_parser.java.rl", "ext/puma_http11/http11_parser.rl", "ext/puma_http11/http11_parser_common.rl", "ext/puma_http11/org/jruby/puma/Http11.java", "ext/puma_http11/org/jruby/puma/Http11Parser.java", "ext/puma_http11/puma_http11.c", "lib/puma.rb", "lib/puma/app/status.rb", "lib/puma/cli.rb", "lib/puma/client.rb", "lib/puma/compat.rb", "lib/puma/configuration.rb", "lib/puma/const.rb", "lib/puma/control_cli.rb", "lib/puma/events.rb", "lib/puma/jruby_restart.rb", "lib/puma/null_io.rb", "lib/puma/rack_patch.rb", "lib/puma/reactor.rb", "lib/puma/server.rb", "lib/puma/thread_pool.rb", "lib/rack/handler/puma.rb", "puma.gemspec", "test/ab_rs.rb", "test/config/app.rb", "test/hello-post.ru", "test/hello.ru", "test/lobster.ru", "test/mime.yaml", "test/slow.ru", "test/test_app_status.rb", "test/test_cli.rb", "test/test_config.rb", "test/test_http10.rb", "test/test_http11.rb", "test/test_integration.rb", "test/test_null_io.rb", "test/test_persistent.rb", "test/test_puma_server.rb", "test/test_rack_handler.rb", "test/test_rack_server.rb", "test/test_thread_pool.rb", "test/test_unix_socket.rb", "test/test_ws.rb", "test/testhelp.rb", "tools/trickletest.rb"] + s.files = ["COPYING", "Gemfile", "Gemfile.lock", "History.txt", "LICENSE", "Manifest.txt", "README.md", "Rakefile", "TODO", "bin/puma", "bin/pumactl", "examples/CA/cacert.pem", "examples/CA/newcerts/cert_1.pem", "examples/CA/newcerts/cert_2.pem", "examples/CA/private/cakeypair.pem", "examples/CA/serial", "examples/config.rb", "examples/puma/cert_puma.pem", "examples/puma/csr_puma.pem", "examples/puma/puma_keypair.pem", "examples/qc_config.rb", "ext/puma_http11/PumaHttp11Service.java", "ext/puma_http11/ext_help.h", "ext/puma_http11/extconf.rb", "ext/puma_http11/http11_parser.c", "ext/puma_http11/http11_parser.h", "ext/puma_http11/http11_parser.java.rl", "ext/puma_http11/http11_parser.rl", "ext/puma_http11/http11_parser_common.rl", "ext/puma_http11/org/jruby/puma/Http11.java", "ext/puma_http11/org/jruby/puma/Http11Parser.java", "ext/puma_http11/puma_http11.c", "lib/puma.rb", "lib/puma/app/status.rb", "lib/puma/cli.rb", "lib/puma/client.rb", "lib/puma/compat.rb", "lib/puma/configuration.rb", "lib/puma/const.rb", "lib/puma/control_cli.rb", "lib/puma/detect.rb", "lib/puma/events.rb", "lib/puma/jruby_restart.rb", "lib/puma/null_io.rb", "lib/puma/rack_patch.rb", "lib/puma/reactor.rb", "lib/puma/server.rb", "lib/puma/thread_pool.rb", "lib/rack/handler/puma.rb", "puma.gemspec", "test/ab_rs.rb", "test/config/app.rb", "test/hello-post.ru", "test/hello.ru", "test/lobster.ru", "test/mime.yaml", "test/slow.ru", "test/test_app_status.rb", "test/test_cli.rb", "test/test_config.rb", "test/test_http10.rb", "test/test_http11.rb", "test/test_integration.rb", "test/test_null_io.rb", "test/test_persistent.rb", "test/test_puma_server.rb", "test/test_rack_handler.rb", "test/test_rack_server.rb", "test/test_thread_pool.rb", "test/test_unix_socket.rb", "test/test_ws.rb", "test/testhelp.rb", "tools/trickletest.rb"] s.homepage = "http://puma.io" s.rdoc_options = ["--main", "README.md"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new(">= 1.8.7") s.rubyforge_project = "puma" - s.rubygems_version = "1.8.22" + s.rubygems_version = "1.8.24" s.summary = "Puma is a simple, fast, and highly concurrent HTTP 1.1 server for Ruby web applications" s.test_files = ["test/test_app_status.rb", "test/test_cli.rb", "test/test_config.rb", "test/test_http10.rb", "test/test_http11.rb", "test/test_integration.rb", "test/test_null_io.rb", "test/test_persistent.rb", "test/test_puma_server.rb", "test/test_rack_handler.rb", "test/test_rack_server.rb", "test/test_thread_pool.rb", "test/test_unix_socket.rb", "test/test_ws.rb"] From b2550acfaf35c9b72b960c68c44c7d1c52d32c58 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Mon, 27 Aug 2012 10:56:43 -0700 Subject: [PATCH 05/13] Handle more errors trying to read client data. Fixes #138 --- lib/puma/reactor.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index ea44bd94..f817d176 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -12,10 +12,12 @@ module Puma @input = [] @sleep_for = DefaultSleepFor @timeouts = [] + + @sockets = [@ready] end def run - sockets = [@ready] + sockets = @sockets while true ready = IO.select sockets, nil, nil, @sleep_for @@ -53,7 +55,7 @@ module Puma @events.parse_error @server, c.env, e - rescue IOError => e + rescue StandardError => e c.close sockets.delete c end @@ -79,12 +81,14 @@ module Puma def run_in_thread @thread = Thread.new { - begin - run - rescue Exception => e - puts "MAJOR ERROR DETECTED" - p e - puts e.backtrace + while true + begin + run + break + rescue StandardError => e + STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})" + puts e.backtrace + end end } end From 074adfbf4d9c735530e242a9da55889cad7624ce Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Mon, 27 Aug 2012 11:02:07 -0700 Subject: [PATCH 06/13] Bump to 1.6.2 --- History.txt | 6 ++++++ lib/puma/const.rb | 2 +- puma.gemspec | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/History.txt b/History.txt index 816f4d79..551cce12 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,9 @@ +=== 1.6.2 / 2012-08-27 + +* 1 bug fix: + * Rescue StandardError instead of IOError to handle SystemCallErrors + as well as other application exceptions inside the reactor. + === 1.6.1 / 2012-07-23 * 1 packaging bug fixed: diff --git a/lib/puma/const.rb b/lib/puma/const.rb index c178c734..9c1b1a7b 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -25,7 +25,7 @@ module Puma # too taxing on performance. module Const - PUMA_VERSION = VERSION = "1.6.1".freeze + PUMA_VERSION = VERSION = "1.6.2".freeze # The default number of seconds for another request within a persistent # session. diff --git a/puma.gemspec b/puma.gemspec index a103fc77..1661517e 100644 --- a/puma.gemspec +++ b/puma.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |s| s.name = "puma" - s.version = "1.6.1" + s.version = "1.6.2" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Evan Phoenix"] - s.date = "2012-08-12" + s.date = "2012-08-27" s.description = "Puma is a simple, fast, and highly concurrent HTTP 1.1 server for Ruby web applications. It can be used with any application that supports Rack, and is considered the replacement for Webrick and Mongrel. It was designed to be the go-to server for [Rubinius](http://rubini.us), but also works well with JRuby and MRI. Puma is intended for use in both development and production environments.\n\nUnder the hood, Puma processes requests using a C-optimized Ragel extension (inherited from Mongrel) that provides fast, accurate HTTP 1.1 protocol parsing in a portable way. Puma then serves the request in a thread from an internal thread pool (which you can control). This allows Puma to provide real concurrency for your web application!\n\nWith Rubinius 2.0, Puma will utilize all cores on your CPU with real threads, meaning you won't have to spawn multiple processes to increase throughput. You can expect to see a similar benefit from JRuby.\n\nOn MRI, there is a Global Interpreter Lock (GIL) that ensures only one thread can be run at a time. But if you're doing a lot of blocking IO (such as HTTP calls to external APIs like Twitter), Puma still improves MRI's throughput by allowing blocking IO to be run concurrently (EventMachine-based servers such as Thin turn off this ability, requiring you to use special libraries). Your mileage may vary. In order to get the best throughput, it is highly recommended that you use a Ruby implementation with real threads like [Rubinius](http://rubini.us) or [JRuby](http://jruby.org)." s.email = ["evan@phx.io"] s.executables = ["puma", "pumactl"] From ed559f02f3f4775e5e18ab082c64aff67caff401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Sun, 2 Sep 2012 21:17:06 +0100 Subject: [PATCH 07/13] Added nginx config sample --- docs/config.md | 0 docs/nginx.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 docs/config.md create mode 100644 docs/nginx.md diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/nginx.md b/docs/nginx.md new file mode 100644 index 00000000..9f63b7f4 --- /dev/null +++ b/docs/nginx.md @@ -0,0 +1,85 @@ +# Nginx configuration example file + +This is a very common setup using an upstream. It was adapted from some Capistrano recipe I found on the Internet a while ago. + +``` +upstream myapp { + server unix:///myapp/tmp/puma.sock; +} + +server { + listen 80; + server_name myapp.com; + + # ~2 seconds is often enough for most folks to parse HTML/CSS and + # retrieve needed images/icons/frames, connections are cheap in + # nginx so increasing this is generally safe... + keepalive_timeout 5; + + # path for static files + root /myapp/public; + access_log /myapp/log/nginx.access.log; + error_log /myapp/log/nginx.error.log info; + + # this rewrites all the requests to the maintenance.html + # page if it exists in the doc root. This is for capistrano's + # disable web task + if (-f $document_root/maintenance.html) { + rewrite ^(.*)$ /maintenance.html last; + break; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + + # If the file exists as a static file serve it directly without + # running all the other rewite tests on it + if (-f $request_filename) { + break; + } + + # check for index.html for directory index + # if its there on the filesystem then rewite + # the url to add /index.html to the end of it + # and then break to send it to the next config rules. + if (-f $request_filename/index.html) { + rewrite (.*) $1/index.html break; + } + + # this is the meat of the rack page caching config + # it adds .html to the end of the url and then checks + # the filesystem for that file. If it exists, then we + # rewite the url to have explicit .html on the end + # and then send it on its way to the next config rule. + # if there is no file on the fs then it sets all the + # necessary headers and proxies to our upstream mongrels + if (-f $request_filename.html) { + rewrite (.*) $1.html break; + } + + if (!-f $request_filename) { + proxy_pass http://myapp; + break; + } + } + + # Now this supposedly should work as it gets the filenames with querystrings that Rails provides. + # BUT there's a chance it could break the ajax calls. + location ~* \.(ico|css|gif|jpe?g|png)(\?[0-9]+)?$ { + expires max; + break; + } + + location ~ ^/javascripts/.*\.js(\?[0-9]+)?$ { + expires max; + break; + } + + # Error pages + # error_page 500 502 503 504 /500.html; + location = /500.html { + root /myapp/current/public; + } +} +``` From 810144e77f073bf16c2d2b8275696fd8cb58069e Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Sun, 2 Sep 2012 23:33:09 -0400 Subject: [PATCH 08/13] Close kept alive sockets on restart. Fixes #144 --- lib/puma/cli.rb | 5 ++-- lib/puma/events.rb | 3 +++ lib/puma/reactor.rb | 15 ++++++++++++ lib/puma/server.rb | 2 ++ test/test_integration.rb | 49 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index c6425fbc..8ff431a8 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -325,12 +325,13 @@ module Puma @listeners << [str, io] when "unix" + path = "#{uri.host}#{uri.path}" + if fd = @inherited_fds.delete(str) log "* Inherited #{str}" - io = server.inherit_unix_listener uri.path, fd + io = server.inherit_unix_listener path, fd else log "* Listening on #{str}" - path = "#{uri.host}#{uri.path}" umask = nil diff --git a/lib/puma/events.rb b/lib/puma/events.rb index ca1a7783..a6dab4da 100644 --- a/lib/puma/events.rb +++ b/lib/puma/events.rb @@ -16,6 +16,9 @@ module Puma def initialize(stdout, stderr) @stdout = stdout @stderr = stderr + + @stdout.sync = true + @stderr.sync = true end attr_reader :stdout, :stderr diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index f817d176..01a57a3d 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -30,6 +30,15 @@ module Puma when "*" sockets += @input @input.clear + when "c" + sockets.delete_if do |s| + if s == @ready + false + else + s.close + true + end + end when "!" return end @@ -121,8 +130,14 @@ module Puma end end + # Close all watched sockets and clear them from being watched + def clear! + @trigger << "c" + end + def shutdown @trigger << "!" + @thread.join end end end diff --git a/lib/puma/server.rb b/lib/puma/server.rb index e4ae4b1b..a79e60a1 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -261,6 +261,8 @@ module Puma end end + @reactor.clear! if @status == :restart + @reactor.shutdown graceful_shutdown if @status == :stop ensure diff --git a/test/test_integration.rb b/test/test_integration.rb index b56e1873..d9920d51 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -1,6 +1,7 @@ require "rbconfig" require 'test/unit' require 'socket' +require 'timeout' require 'puma/cli' require 'puma/control_cli' @@ -10,12 +11,35 @@ class TestIntegration < Test::Unit::TestCase @state_path = "test/test_puma.state" @bind_path = "test/test_server.sock" @control_path = "test/test_control.sock" + @tcp_port = 9998 + + @server = nil end def teardown File.unlink @state_path rescue nil File.unlink @bind_path rescue nil File.unlink @control_path rescue nil + + if @server + Process.kill "INT", @server.pid + Process.wait @server.pid + @server.close + end + end + + def server(opts) + core = "#{Gem.ruby} -rubygems -Ilib bin/puma" + cmd = "#{core} --restart-cmd '#{core}' -b tcp://127.0.0.1:#{@tcp_port} #{opts}" + @server = IO.popen(cmd, "r") + + true until @server.gets =~ /Ctrl-C/ + + @server + end + + def signal(which) + Process.kill which, @server.pid end def test_stop_via_pumactl @@ -45,4 +69,29 @@ class TestIntegration < Test::Unit::TestCase assert_kind_of Thread, t.join(1), "server didn't stop" end + + def test_restart_closes_keepalive_sockets + server("-q test/hello.ru") + + s = TCPSocket.new "localhost", @tcp_port + s << "GET / HTTP/1.1\r\n\r\n" + true until s.gets == "\r\n" + + s.readpartial(20) + signal :USR2 + + true until @server.gets =~ /Ctrl-C/ + + s.write "GET / HTTP/1.1\r\n\r\n" + + assert_raises Errno::ECONNRESET do + Timeout.timeout(2) do + s.read(2) + end + end + + s = TCPSocket.new "localhost", @tcp_port + s << "GET / HTTP/1.0\r\n\r\n" + assert_equal "Hello World", s.read.split("\r\n").last + end end From 4121e4c1941aefa13b4dc19354be6a3396401df1 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Mon, 3 Sep 2012 12:18:46 -0400 Subject: [PATCH 09/13] Try to fix hung test on travis --- test/test_integration.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_integration.rb b/test/test_integration.rb index d9920d51..a403cbc5 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -2,6 +2,7 @@ require "rbconfig" require 'test/unit' require 'socket' require 'timeout' +require 'net/http' require 'puma/cli' require 'puma/control_cli' @@ -33,8 +34,7 @@ class TestIntegration < Test::Unit::TestCase cmd = "#{core} --restart-cmd '#{core}' -b tcp://127.0.0.1:#{@tcp_port} #{opts}" @server = IO.popen(cmd, "r") - true until @server.gets =~ /Ctrl-C/ - + sleep 1 @server end @@ -80,8 +80,6 @@ class TestIntegration < Test::Unit::TestCase s.readpartial(20) signal :USR2 - true until @server.gets =~ /Ctrl-C/ - s.write "GET / HTTP/1.1\r\n\r\n" assert_raises Errno::ECONNRESET do From c47fcc80c891963b9fc0b01e15d3ce3d83d2910d Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Mon, 3 Sep 2012 12:23:42 -0400 Subject: [PATCH 10/13] Another kludge for travis --- test/test_integration.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_integration.rb b/test/test_integration.rb index a403cbc5..9a0d789a 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -80,6 +80,8 @@ class TestIntegration < Test::Unit::TestCase s.readpartial(20) signal :USR2 + sleep 3 + s.write "GET / HTTP/1.1\r\n\r\n" assert_raises Errno::ECONNRESET do From 06926d0c9425b74ef10392e2b9a567cc1d6baef9 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Tue, 4 Sep 2012 13:13:18 -0400 Subject: [PATCH 11/13] Bump to 1.6.3 --- History.txt | 6 ++++++ lib/puma/const.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/History.txt b/History.txt index 551cce12..0dbf269c 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,9 @@ +=== 1.6.3 / 2012-09-04 + +* 1 bug fix: + * Close sockets waiting in the reactor when a hot restart is performed + so that browsers reconnect on the next request + === 1.6.2 / 2012-08-27 * 1 bug fix: diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 9c1b1a7b..f3150a9c 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -25,7 +25,7 @@ module Puma # too taxing on performance. module Const - PUMA_VERSION = VERSION = "1.6.2".freeze + PUMA_VERSION = VERSION = "1.6.3".freeze # The default number of seconds for another request within a persistent # session. From a75fb46e7b7c87846ee38fa0dd001f7b2cbf36a7 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 5 Sep 2012 17:20:41 -0700 Subject: [PATCH 12/13] Update gemspec --- puma.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/puma.gemspec b/puma.gemspec index 1661517e..e2623e80 100644 --- a/puma.gemspec +++ b/puma.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |s| s.name = "puma" - s.version = "1.6.2" + s.version = "1.6.3" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Evan Phoenix"] - s.date = "2012-08-27" + s.date = "2012-09-04" s.description = "Puma is a simple, fast, and highly concurrent HTTP 1.1 server for Ruby web applications. It can be used with any application that supports Rack, and is considered the replacement for Webrick and Mongrel. It was designed to be the go-to server for [Rubinius](http://rubini.us), but also works well with JRuby and MRI. Puma is intended for use in both development and production environments.\n\nUnder the hood, Puma processes requests using a C-optimized Ragel extension (inherited from Mongrel) that provides fast, accurate HTTP 1.1 protocol parsing in a portable way. Puma then serves the request in a thread from an internal thread pool (which you can control). This allows Puma to provide real concurrency for your web application!\n\nWith Rubinius 2.0, Puma will utilize all cores on your CPU with real threads, meaning you won't have to spawn multiple processes to increase throughput. You can expect to see a similar benefit from JRuby.\n\nOn MRI, there is a Global Interpreter Lock (GIL) that ensures only one thread can be run at a time. But if you're doing a lot of blocking IO (such as HTTP calls to external APIs like Twitter), Puma still improves MRI's throughput by allowing blocking IO to be run concurrently (EventMachine-based servers such as Thin turn off this ability, requiring you to use special libraries). Your mileage may vary. In order to get the best throughput, it is highly recommended that you use a Ruby implementation with real threads like [Rubinius](http://rubini.us) or [JRuby](http://jruby.org)." s.email = ["evan@phx.io"] s.executables = ["puma", "pumactl"] From bd5d824ce5c9949bce4c9330bb24f2336a4e2264 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 5 Sep 2012 22:09:42 -0700 Subject: [PATCH 13/13] Write 400 on HTTP parse error. Fixes #142 --- lib/puma/client.rb | 14 ++++++++++++++ lib/puma/const.rb | 6 ++++++ lib/puma/reactor.rb | 5 ++++- lib/puma/server.rb | 6 ++++++ test/test_integration.rb | 29 +++++++++++++++++++++++++++-- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/puma/client.rb b/lib/puma/client.rb index a51eca90..0136a4ea 100644 --- a/lib/puma/client.rb +++ b/lib/puma/client.rb @@ -229,5 +229,19 @@ module Puma false end + + def write_400 + begin + @io << ERROR_400_RESPONSE + rescue StandardError + end + end + + def write_500 + begin + @io << ERROR_500_RESPONSE + rescue StandardError + end + end end end diff --git a/lib/puma/const.rb b/lib/puma/const.rb index f3150a9c..a514b6a9 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -47,11 +47,17 @@ module Puma PUMA_TMP_BASE = "puma".freeze + # Indicate that we couldn't parse the request + ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n" + # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff. ERROR_404_RESPONSE = "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze CONTENT_LENGTH = "CONTENT_LENGTH".freeze + # Indicate that there was an internal error, obviously. + ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n" + # A common header for indicating the server is too busy. Not used yet. ERROR_503_RESPONSE = "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 01a57a3d..604e439e 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -59,13 +59,16 @@ module Puma # The client doesn't know HTTP well rescue HttpParserError => e + c.write_400 c.close + sockets.delete c @events.parse_error @server, c.env, e - rescue StandardError => e + c.write_500 c.close + sockets.delete c end end diff --git a/lib/puma/server.rb b/lib/puma/server.rb index a79e60a1..9906e216 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -206,7 +206,9 @@ module Puma begin process_now = client.eagerly_finish rescue HttpParserError => e + client.write_400 client.close + @events.parse_error self, client.env, e rescue IOError client.close @@ -325,10 +327,14 @@ module Puma # The client doesn't know HTTP well rescue HttpParserError => e + client.write_400 + @events.parse_error self, client.env, e # Server error rescue StandardError => e + client.write_500 + @events.unknown_error self, e, "Read" ensure diff --git a/test/test_integration.rb b/test/test_integration.rb index 9a0d789a..0f535b1c 100644 --- a/test/test_integration.rb +++ b/test/test_integration.rb @@ -3,6 +3,7 @@ require 'test/unit' require 'socket' require 'timeout' require 'net/http' +require 'tempfile' require 'puma/cli' require 'puma/control_cli' @@ -15,6 +16,7 @@ class TestIntegration < Test::Unit::TestCase @tcp_port = 9998 @server = nil + @script = nil end def teardown @@ -27,14 +29,27 @@ class TestIntegration < Test::Unit::TestCase Process.wait @server.pid @server.close end + + if @script + @script.close! + end end def server(opts) core = "#{Gem.ruby} -rubygems -Ilib bin/puma" cmd = "#{core} --restart-cmd '#{core}' -b tcp://127.0.0.1:#{@tcp_port} #{opts}" - @server = IO.popen(cmd, "r") + tf = Tempfile.new "puma-test" + tf.puts "exec #{cmd}" + tf.close + + @script = tf + + @server = IO.popen("sh #{tf.path}", "r") + + true while @server.gets =~ /Ctrl-C/ sleep 1 + @server end @@ -80,7 +95,8 @@ class TestIntegration < Test::Unit::TestCase s.readpartial(20) signal :USR2 - sleep 3 + true while @server.gets =~ /Ctrl-C/ + sleep 1 s.write "GET / HTTP/1.1\r\n\r\n" @@ -94,4 +110,13 @@ class TestIntegration < Test::Unit::TestCase s << "GET / HTTP/1.0\r\n\r\n" assert_equal "Hello World", s.read.split("\r\n").last end + + def test_bad_query_string_outputs_400 + server "-q test/hello.ru 2>&1" + + s = TCPSocket.new "localhost", @tcp_port + s << "GET /?h=% HTTP/1.0\r\n\r\n" + data = s.read + assert_equal "HTTP/1.1 400 Bad Request\r\n\r\n", data + end end