From 37a12997628fcab722512f8a6370b92d44e33529 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 2 Oct 2009 20:44:03 -0700 Subject: initial revision No tests yet, but the old "gossamer" and "rainbows" branches seem to be basically working. --- .document | 6 + .gitignore | 18 ++ COPYING | 339 +++++++++++++++++++++++++++++++++++++ Documentation/.gitignore | 5 + Documentation/GNUmakefile | 30 ++++ Documentation/rainbows.1.txt | 159 +++++++++++++++++ GIT-VERSION-GEN | 41 +++++ GNUmakefile | 156 +++++++++++++++++ LICENSE | 55 ++++++ README | 69 ++++++++ Rakefile | 103 +++++++++++ SIGNALS | 94 ++++++++++ bin/rainbows | 166 ++++++++++++++++++ lib/rainbows.rb | 21 +++ lib/rainbows/configurator.rb | 25 +++ lib/rainbows/const.rb | 22 +++ lib/rainbows/http_response.rb | 35 ++++ lib/rainbows/http_server.rb | 34 ++++ lib/rainbows/revactor.rb | 115 +++++++++++++ lib/rainbows/revactor/tee_input.rb | 44 +++++ lib/rainbows/thread_base.rb | 60 +++++++ lib/rainbows/thread_pool.rb | 84 +++++++++ local.mk.sample | 54 ++++++ rainbows.gemspec | 47 +++++ 24 files changed, 1782 insertions(+) create mode 100644 .document create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 Documentation/.gitignore create mode 100644 Documentation/GNUmakefile create mode 100644 Documentation/rainbows.1.txt create mode 100755 GIT-VERSION-GEN create mode 100644 GNUmakefile create mode 100644 LICENSE create mode 100644 README create mode 100644 Rakefile create mode 100644 SIGNALS create mode 100644 bin/rainbows create mode 100644 lib/rainbows.rb create mode 100644 lib/rainbows/configurator.rb create mode 100644 lib/rainbows/const.rb create mode 100644 lib/rainbows/http_response.rb create mode 100644 lib/rainbows/http_server.rb create mode 100644 lib/rainbows/revactor.rb create mode 100644 lib/rainbows/revactor/tee_input.rb create mode 100644 lib/rainbows/thread_base.rb create mode 100644 lib/rainbows/thread_pool.rb create mode 100644 local.mk.sample create mode 100644 rainbows.gemspec diff --git a/.document b/.document new file mode 100644 index 0000000..88e5ce8 --- /dev/null +++ b/.document @@ -0,0 +1,6 @@ +README +LICENSE +lib +rainbows.1 +ChangeLog +NEWS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9cb887 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.bundle +*.log +*.so +*.rbc +.DS_Store +/.config +/InstalledFiles +/doc +/local.mk +/test/install-* +log/ +pkg/ +/vendor +/NEWS +/ChangeLog +/.manifest +/GIT-VERSION-FILE +/man diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Documentation/.gitignore b/Documentation/.gitignore new file mode 100644 index 0000000..46679d6 --- /dev/null +++ b/Documentation/.gitignore @@ -0,0 +1,5 @@ +*.1 +*.5 +*.7 +*.gz +*.html diff --git a/Documentation/GNUmakefile b/Documentation/GNUmakefile new file mode 100644 index 0000000..e33c87f --- /dev/null +++ b/Documentation/GNUmakefile @@ -0,0 +1,30 @@ +all:: + +PANDOC = pandoc +PANDOC_OPTS = -f markdown --email-obfuscation=none --sanitize-html +pandoc = $(PANDOC) $(PANDOC_OPTS) +pandoc_html = $(pandoc) --toc -t html --no-wrap + +man1 := $(addsuffix .1,rainbows) +html1 := $(addsuffix .html,$(man1)) + +all:: html man + +html: $(html1) +man: $(man1) + +install-html: html + mkdir -p ../doc/man1 + install -m 644 $(html1) ../doc/man1 + +install-man: man + mkdir -p ../man/man1 + install -m 644 $(man1) ../man/man1 + +%.1: %.1.txt + $(pandoc) -s -t man < $< > $@+ && mv $@+ $@ +%.1.html: %.1.txt + $(pandoc_html) < $< > $@+ && mv $@+ $@ + +clean:: + $(RM) $(man1) $(html1) diff --git a/Documentation/rainbows.1.txt b/Documentation/rainbows.1.txt new file mode 100644 index 0000000..6577203 --- /dev/null +++ b/Documentation/rainbows.1.txt @@ -0,0 +1,159 @@ +% UNICORN-RAINBOWS(1) Unicorn User Manual +% The Unicorn Community +% September 15, 2009 + +# NAME + +unicorn-rainbows - rackup-like command to launch Unicorn Rainbows + +# SYNOPSIS + +unicorn-rainbows [-c CONFIG_FILE] [-E RACK_ENV] [-D] [RACKUP_FILE] + +# DESCRIPTION + +A rackup(1)-like command to launch Rack applications using Unicorn +Rainbows. It is expected to be started in your application root +(APP_ROOT), but "Dir.chdir" may also be executed in the CONFIG_FILE or +RACKUP_FILE. + +While Unicorn Rainbows takes a myriad of command-line options for +compatibility with ruby(1) and rackup(1), it is recommended to stick +to the few command-line options specified in the SYNOPSIS and use +the CONFIG_FILE as much as possible. + +# RACKUP FILE + +This defaults to \"config.ru\" in APP_ROOT. It should be the same +file used by rackup(1) and other Rack launchers, it uses the +*Rack::Builder* DSL. + +Embedded command-line options are mostly parsed for compatibility +with rackup(1) but strongly discouraged. + +# UNICORN OPTIONS +-c, \--config-file CONFIG_FILE +: Path to the Unicorn-specific config file. The config file is + implemented as a Ruby DSL, so Ruby code may executed (e.g. + "Dir.chdir", "Process::UID.change_privilege"). See the RDoc/ri + for the *Unicorn::Configurator* class for the full list of + directives available from the DSL. + +-D, \--daemonize +: Run daemonized in the background. The process is detached from + the controlling terminal and stdin is redirected to "/dev/null". + Unlike many common UNIX daemons, we do not chdir to \"/\" + upon daemonization to allow more control over the startup/upgrade + process. + Unless specified in the CONFIG_FILE, stderr and stdout will + also be redirected to "/dev/null". + +-E, \--env RACK_ENV +: Run under the given RACK_ENV. See the RACK ENVIRONMENT section + for more details. + +-l, \--listen ADDRESS +: Listens on a given ADDRESS. ADDRESS may be in the form of + HOST:PORT or PATH, HOST:PORT is taken to mean a TCP socket + and PATH is meant to be a path to a UNIX domain socket. + Defaults to "0.0.0.0:8080" (all addresses on TCP port 8080) + For production deployments, specifying the "listen" directive in + CONFIG_FILE is recommended as it allows fine-tuning of socket + options. + +# RACKUP COMPATIBILITY OPTIONS +-o, \--host HOST +: Listen on a TCP socket belonging to HOST, default is + "0.0.0.0" (all addresses). + If specified multiple times on the command-line, only the + last-specified value takes effect. + This option only exists for compatibility with the rackup(1) command, + use of "-l"/"\--listen" switch is recommended instead. + +-p, \--port PORT +: Listen on the specified TCP PORT, default is 8080. + If specified multiple times on the command-line, only the last-specified + value takes effect. + This option only exists for compatibility with the rackup(1) command, + use of "-l"/"\--listen" switch is recommended instead. + +-s, \--server SERVER +: No-op, this exists only for compatibility with rackup(1). + +# RUBY OPTIONS +-e, \--eval LINE +: Evaluate a LINE of Ruby code. This evaluation happens + immediately as the command-line is being parsed. + +-d, \--debug +: Turn on debug mode, the $DEBUG variable is set to true. + +-w, \--warn +: Turn on verbose warnings, the $VERBOSE variable is set to true. + +-I, \--include PATH +: specify $LOAD_PATH. PATH will be prepended to $LOAD_PATH. + The \':\' character may be used to delimit multiple directories. + This directive may be used more than once. Modifications to + $LOAD_PATH take place immediately and in the order they were + specified on the command-line. + +-r, \--require LIBRARY +: require a specified LIBRARY before executing the application. The + \"require\" statement will be executed immediately and in the order + they were specified on the command-line. + +# SIGNALS + +The following UNIX signals may be sent to the master process: + +* HUP - reload config file, app, and gracefully restart all workers +* INT/TERM - quick shutdown, kills all workers immediately +* QUIT - graceful shutdown, waits for workers to finish their + current request before finishing. +* USR1 - reopen all logs owned by the master and all workers + See Unicorn::Util.reopen_logs for what is considered a log. +* USR2 - reexecute the running binary. A separate QUIT + should be sent to the original process once the child is verified to + be up and running. +* WINCH - gracefully stops workers but keep the master running. + This will only work for daemonized processes. +* TTIN - increment the number of worker processes by one +* TTOU - decrement the number of worker processes by one + +See the [SIGNALS][4] document for full description of all signals +used by Unicorn Rainbows. + +# RACK ENVIRONMENT + +Accepted values of RACK_ENV and the middleware they automatically load +(outside of RACKUP_FILE) are exactly as those in rackup(1): + +* development - loads Rack::CommonLogger, Rack::ShowExceptions, and + Rack::Lint middleware +* deployment - loads Rack::CommonLogger middleware +* none - loads no middleware at all, relying + entirely on RACKUP_FILE + +All unrecognized values for RACK_ENV are assumed to be +"none". Production deployments are strongly encouraged to use +"deployment" or "none" for maximum performance. + +Note that the Rack::ContentLength and Rack::Chunked middlewares +are never loaded by default. If needed, they should be +individually specified in the RACKUP_FILE, some frameworks do +not require them. + +# SEE ALSO + +* unicorn(1) +* *Rack::Builder* ri/RDoc +* *Unicorn::Configurator* ri/RDoc +* [Unicorn Rainbows RDoc][1] +* [Rack RDoc][2] +* [Rackup HowTo][3] + +[1]: http://rainbows.rubyforge.org/ +[2]: http://rack.rubyforge.org/doc/ +[3]: http://wiki.github.com/rack/rack/tutorial-rackup-howto +[4]: http://rainbows.rubyforge.org/SIGNALS.html diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN new file mode 100755 index 0000000..a501bce --- /dev/null +++ b/GIT-VERSION-GEN @@ -0,0 +1,41 @@ +#!/bin/sh + +GVF=GIT-VERSION-FILE +DEF_VER=v0.93.0 +# DEF_VER=v0.93.0.GIT + +LF=' +' + +# First see if there is a version file (included in release tarballs), +# then try git-describe, then default. +if test -f version +then + VN=$(cat version) || VN="$DEF_VER" +elif test -d .git -o -f .git && + VN=$(git describe --abbrev=4 HEAD 2>/dev/null) && + case "$VN" in + *$LF*) (exit 1) ;; + v[0-9]*) + git update-index -q --refresh + test -z "$(git diff-index --name-only HEAD --)" || + VN="$VN-dirty" ;; + esac +then + VN=$(echo "$VN" | sed -e 's/-/./g'); +else + VN="$DEF_VER" +fi + +VN=$(expr "$VN" : v*'\(.*\)') + +if test -r $GVF +then + VC=$(sed -e 's/^GIT_VERSION = //' <$GVF) +else + VC=unset +fi +test "$VN" = "$VC" || { + echo >&2 "GIT_VERSION = $VN" + echo "GIT_VERSION = $VN" >$GVF +} diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..63ba4b1 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,156 @@ +# use GNU Make to run tests in parallel, and without depending on Rubygems +all:: +ruby = ruby +rake = rake +GIT_URL = git://git.bogomips.org/rainbows.git + +GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE + @./GIT-VERSION-GEN +-include GIT-VERSION-FILE +-include local.mk +ruby_bin := $(shell which $(ruby)) +ifeq ($(DLEXT),) # "so" for Linux + DLEXT := $(shell $(ruby) -rrbconfig -e 'puts Config::CONFIG["DLEXT"]') +endif +ifeq ($(RUBY_VERSION),) + RUBY_VERSION := $(shell $(ruby) -e 'puts RUBY_VERSION') +endif + +base_bins := rainbows +bins := $(addprefix bin/, $(base_bins)) +man1_bins := $(addsuffix .1, $(base_bins)) +man1_paths := $(addprefix man/man1/, $(man1_bins)) + +install: $(bins) + $(prep_setup_rb) + $(RM) -r .install-tmp + mkdir .install-tmp + cp -p bin/* .install-tmp + $(ruby) setup.rb all + $(RM) $^ + mv .install-tmp/* bin/ + $(RM) -r .install-tmp + $(prep_setup_rb) + +setup_rb_files := .config InstalledFiles +prep_setup_rb := @-$(RM) $(setup_rb_files);$(MAKE) -C $(ext) clean + +clean: + -$(MAKE) -C $(ext) clean + -$(MAKE) -C Documentation clean + $(RM) $(setup_rb_files) $(t_log) + +man: + $(MAKE) -C Documentation install-man + +pkg_extra := GIT-VERSION-FILE NEWS ChangeLog +manifest: $(pkg_extra) man + $(RM) .manifest + $(MAKE) .manifest + +.manifest: + (git ls-files && \ + for i in $@ $(pkg_extra) $(man1_paths); \ + do echo $$i; done) | LC_ALL=C sort > $@+ + cmp $@+ $@ || mv $@+ $@ + $(RM) $@+ + +NEWS: GIT-VERSION-FILE + $(rake) -s news_rdoc > $@+ + mv $@+ $@ + +SINCE = +ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..) +ChangeLog: GIT-VERSION-FILE + @echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+ + @echo >> $@+ + git log $(log_range) | sed -e 's/^/ /' >> $@+ + mv $@+ $@ + +news_atom := http://rainbows.rubyforge.org/NEWS.atom.xml +cgit_atom := http://git.bogomips.org/cgit/rainbows.git/atom/?h=master +atom = + +# using rdoc 2.4.1+ +doc: .document NEWS ChangeLog + for i in $(man1_bins); do > $$i; done + rdoc -Na -t "$(shell sed -ne '1s/^= //p' README)" + install -m644 $(shell grep '^[A-Z]' .document) doc/ + $(MAKE) -C Documentation install-html install-man + install -m644 $(man1_paths) doc/ + cd doc && for i in $(base_bins); do \ + sed -e '/"documentation">/r man1/'$$i'.1.html' \ + < $${i}_1.html > tmp && mv tmp $${i}_1.html; done + $(ruby) -i -p -e \ + '$$_.gsub!("",%q{\&$(call atom,$(cgit_atom))})' \ + doc/ChangeLog.html + $(ruby) -i -p -e \ + '$$_.gsub!("",%q{\&$(call atom,$(news_atom))})' \ + doc/NEWS.html doc/README.html + $(rake) -s news_atom > doc/NEWS.atom.xml + cd doc && ln README.html tmp && mv tmp index.html + $(RM) $(man1_bins) + +ifneq ($(VERSION),) +rfproject := rainbows +rfpackage := rainbows +pkggem := pkg/$(rfpackage)-$(VERSION).gem +pkgtgz := pkg/$(rfpackage)-$(VERSION).tgz +release_notes := release_notes-$(VERSION) +release_changes := release_changes-$(VERSION) + +release-notes: $(release_notes) +release-changes: $(release_changes) +$(release_changes): + $(rake) -s release_changes > $@+ + $(VISUAL) $@+ && test -s $@+ && mv $@+ $@ +$(release_notes): + GIT_URL=$(GIT_URL) $(rake) -s release_notes > $@+ + $(VISUAL) $@+ && test -s $@+ && mv $@+ $@ + +# ensures we're actually on the tagged $(VERSION), only used for release +verify: + test x"$(shell umask)" = x0022 + git rev-parse --verify refs/tags/v$(VERSION)^{} + git diff-index --quiet HEAD^0 + test `git rev-parse --verify HEAD^0` = \ + `git rev-parse --verify refs/tags/v$(VERSION)^{}` + +fix-perms: + -git ls-tree -r HEAD | awk '/^100644 / {print $$NF}' | xargs chmod 644 + -git ls-tree -r HEAD | awk '/^100755 / {print $$NF}' | xargs chmod 755 + +gem: $(pkggem) + +install-gem: $(pkggem) + gem install $(CURDIR)/$< + +$(pkggem): manifest fix-perms + gem build $(rfpackage).gemspec + mkdir -p pkg + mv $(@F) $@ + +$(pkgtgz): distdir = $(basename $@) +$(pkgtgz): HEAD = v$(VERSION) +$(pkgtgz): manifest fix-perms + @test -n "$(distdir)" + $(RM) -r $(distdir) + mkdir -p $(distdir) + tar c `cat .manifest` | (cd $(distdir) && tar x) + cd pkg && tar c $(basename $(@F)) | gzip -9 > $(@F)+ + mv $@+ $@ + +package: $(pkgtgz) $(pkggem) + +release: verify package $(release_notes) $(release_changes) + rubyforge add_release -f -n $(release_notes) -a $(release_changes) \ + $(rfproject) $(rfpackage) $(VERSION) $(pkggem) + rubyforge add_file \ + $(rfproject) $(rfpackage) $(VERSION) $(pkgtgz) +else +gem install-gem: GIT-VERSION-FILE + $(MAKE) $@ VERSION=$(GIT_VERSION) +endif + +.PHONY: .FORCE-GIT-VERSION-FILE doc manifest man diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cef611a --- /dev/null +++ b/LICENSE @@ -0,0 +1,55 @@ +Rainbows is copyrighted free software by Eric Wong +(mailto:normalperson@yhbt.net) and contributors. You can redistribute it +and/or modify it under either the terms of the +{GPL2}[http://www.gnu.org/licenses/gpl-2.0.txt] (see link:COPYING) or +the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or by allowing the author to include your + modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or executable + form, provided that you do at least ONE of the following: + + a) distribute the executables and library files of the software, + together with instructions (in the manual page or equivalent) on where + to get the original distribution. + + b) accompany the distribution with the machine-readable source of the + software. + + c) give non-standard executables non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under this terms. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. diff --git a/README b/README new file mode 100644 index 0000000..14ba504 --- /dev/null +++ b/README @@ -0,0 +1,69 @@ += Rainbows: Unicorn for Comet and slow clients + +Rainbows is a HTTP server for {Rack}[http://rack.rubyforge.org/] +applications. It is based on {Unicorn}[http://unicorn.bogomips.org/], +but designed to handle applications that expect long request/response +times and/or slow clients. It is designed for Comet applications and +reverse proxy implementations. For Rack applications not heavily +bound by external dependencies, consider Unicorn instead as it simpler +and easier to debug. + +== Features + +* Designed for Rack, the standard for modern Ruby HTTP applications. + +* Built on Unicorn, inheriting its process/socket management features + such as transparent upgrades and Ruby configuration DSL. + +* Like Unicorn, it is able to stream large request bodies off the + socket to the application while the client is still uploading. + +* Combines heavyweight concurrency (worker processes) with lightweight + concurrency (Actors/Threads), allowing CPU/memory/disk to be scaled + independently of client connections. + +== License + +Rainbows is copyright 2009 Eric Wong and contributors. It is based on +Mongrel and Unicorn and carries the same license. + +Mongrel is copyright 2007 Zed A. Shaw and contributors. It is licensed +under the Ruby license and the GPL2. See the included LICENSE file for +details. + +Rainbows is 100% Free Software. + +== Usage + +=== for Rack applications + +In APP_ROOT (where config.ru is located), run: + + rainbows + +Rainbows will bind to all interfaces on TCP port 8080 by default. + +=== Configuration File(s) + +Rainbows will look for the config.ru file used by rackup in APP_ROOT. + +For deployments, it can use a config file for Rainbows-specific options +specified by the +--config-file/-c+ command-line switch. See +Rainbows::Configurator for the syntax of the Rainbows-specific options. + +== Disclaimer + +There is NO WARRANTY whatsoever if anything goes wrong, but let us know +and we'll try our best to fix it. + +== Contact + +All feedback (bug reports, user/development dicussion, patches, pull +requests) go to the mailing list/newsgroup. Patches must be sent inline +(git format-patch -M + git send-email). No subscription is necessary +to post on the mailing list. No top posting. Address replies +To:+ (or ++Cc:+) the original sender and +Cc:+ the mailing list. + +* email: mailto:rainbows-talk@rubyforge.org +* archives: http://rubyforge.org/pipermail/rainbows-talk +* subscribe: http://rubyforge.org/mailman/listinfo/rainbows-talk diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f638459 --- /dev/null +++ b/Rakefile @@ -0,0 +1,103 @@ +# -*- encoding: binary -*- + +# most tasks are in the GNUmakefile which offers better parallelism + +def tags + timefmt = '%Y-%m-%dT%H:%M:%SZ' + @tags ||= `git tag -l`.split(/\n/).map do |tag| + if %r{\Av[\d\.]+\z} =~ tag + header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3) + header = header.split(/\n/) + tagger = header.grep(/\Atagger /).first + body ||= "initial" + { + :time => Time.at(tagger.split(/ /)[-2].to_i).utc.strftime(timefmt), + :tagger_name => %r{^tagger ([^<]+)}.match(tagger)[1], + :tagger_email => %r{<([^>]+)>}.match(tagger)[1], + :id => `git rev-parse refs/tags/#{tag}`.chomp!, + :tag => tag, + :subject => subject, + :body => body, + } + end + end.compact.sort { |a,b| b[:time] <=> a[:time] } +end + +cgit_url = "http://git.bogomips.org/cgit/rainbows.git" + +desc 'prints news as an Atom feed' +task :news_atom do + require 'nokogiri' + new_tags = tags[0,10] + puts(Nokogiri::XML::Builder.new do + feed :xmlns => "http://www.w3.org/2005/Atom" do + id! "http://rainbows.rubyforge.org/NEWS.atom.xml" + title "Rainbows news" + subtitle "Unicorn for Comet and slow clients" + link! :rel => 'alternate', :type => 'text/html', + :href => 'http://rainbows.rubyforge.org/NEWS.html' + updated(new_tags.empty? ? "1970-01-01T00:00:00Z" : new_tags.first[:time]) + new_tags.each do |tag| + entry do + title tag[:subject] + updated tag[:time] + published tag[:time] + author { + name tag[:tagger_name] + email tag[:tagger_email] + } + url = "#{cgit_url}/tag/?id=#{tag[:tag]}" + link! :rel => "alternate", :type => "text/html", :href =>url + id! url + content(:type => 'text') { tag[:body] } + end + end + end + end.to_xml) +end + +desc 'prints RDoc-formatted news' +task :news_rdoc do + tags.each do |tag| + time = tag[:time].tr!('T', ' ').gsub!(/:\d\dZ/, ' UTC') + puts "=== #{tag[:tag].sub(/^v/, '')} / #{time}" + puts "" + + body = tag[:body] + puts tag[:body].gsub(/^/sm, " ").gsub!(/[ \t]+$/sm, "") + puts "" + end +end + +desc "print release changelog for Rubyforge" +task :release_changes do + version = ENV['VERSION'] or abort "VERSION= needed" + version = "v#{version}" + vtags = tags.map { |tag| tag[:tag] =~ /\Av/ and tag[:tag] }.sort + prev = vtags[vtags.index(version) - 1] + if prev + system('git', 'diff', '--stat', prev, version) or abort $? + puts "" + system('git', 'log', "#{prev}..#{version}") or abort $? + else + system('git', 'log', version) or abort $? + end +end + +desc "print release notes for Rubyforge" +task :release_notes do + require 'rubygems' + + git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/rainbows.git' + + spec = Gem::Specification.load('rainbows.gemspec') + puts spec.description.strip + puts "" + puts "* #{spec.homepage}" + puts "* #{spec.email}" + puts "* #{git_url}" + + _, _, body = `git cat-file tag v#{spec.version}`.split(/\n\n/, 3) + print "\nChanges:\n\n" + puts body +end diff --git a/SIGNALS b/SIGNALS new file mode 100644 index 0000000..fe68fa9 --- /dev/null +++ b/SIGNALS @@ -0,0 +1,94 @@ +== Signal handling + +In general, signals need only be sent to the master process. However, the +signals Rainbows uses internally to communicate with the worker processes are +documented here as well. + +=== Master Process + +* HUP - reload config file, app, and gracefully restart all workers + +* INT/TERM - quick shutdown, kills all workers immediately + +* QUIT - graceful shutdown, waits for workers to finish their + current request before finishing. + +* USR1 - reopen all logs owned by the master and all workers + See Unicorn::Util.reopen_logs for what is considered a log. + +* USR2 - reexecute the running binary. A separate QUIT + should be sent to the original process once the child is verified to + be up and running. + +* WINCH - gracefully stops workers but keep the master running. + This will only work for daemonized processes. + +* TTIN - increment the number of worker processes by one + +* TTOU - decrement the number of worker processes by one + +=== Worker Processes + +Sending signals directly to the worker processes should not normally be +needed. If the master process is running, any exited worker will be +automatically respawned. + +* INT/TERM - Quick shutdown, immediately exit. + Unless WINCH has been sent to the master (or the master is killed), + the master process will respawn a worker to replace this one. + +* QUIT - Gracefully exit after finishing the current request. + Unless WINCH has been sent to the master (or the master is killed), + the master process will respawn a worker to replace this one. + +* USR1 - Reopen all logs owned by the worker process. + See Unicorn::Util.reopen_logs for what is considered a log. + Log files are not reopened until it is done processing + the current request, so multiple log lines for one request + (as done by Rails) will not be split across multiple logs. + +=== Procedure to replace a running rainbows executable + +You may replace a running instance of unicorn with a new one without +losing any incoming connections. Doing so will reload all of your +application code, Unicorn config, Ruby executable, and all libraries. +The only things that will not change (due to OS limitations) are: + +1. The path to the rainbows executable script. If you want to change to + a different installation of Ruby, you can modify the shebang + line to point to your alternative interpreter. + +The procedure is exactly like that of nginx: + +1. Send USR2 to the master process + +2. Check your process manager or pid files to see if a new master spawned + successfully. If you're using a pid file, the old process will have + ".oldbin" appended to its path. You should have two master instances + of rainbows running now, both of which will have workers servicing + requests. Your process tree should look something like this: + + rainbows master (old) + \_ rainbows worker[0] + \_ rainbows worker[1] + \_ rainbows worker[2] + \_ rainbows worker[3] + \_ rainbows master + \_ rainbows worker[0] + \_ rainbows worker[1] + \_ rainbows worker[2] + \_ rainbows worker[3] + +3. You can now send WINCH to the old master process so only the new workers + serve requests. If your rainbows process is bound to an + interactive terminal, you can skip this step. Step 5 will be more + difficult but you can also skip it if your process is not daemonized. + +4. You should now ensure that everything is running correctly with the + new workers as the old workers die off. + +5. If everything seems ok, then send QUIT to the old master. You're done! + + If something is broken, then send HUP to the old master to reload + the config and restart its workers. Then send QUIT to the new master + process. diff --git a/bin/rainbows b/bin/rainbows new file mode 100644 index 0000000..b3f3a26 --- /dev/null +++ b/bin/rainbows @@ -0,0 +1,166 @@ +#!/home/ew/bin/ruby +# -*- encoding: binary -*- +require 'unicorn/launcher' +require 'rainbows' +require 'optparse' + +env = "development" +daemonize = false +listeners = [] +options = { :listeners => listeners } +host, port = Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT +set_listener = false + +opts = OptionParser.new("", 24, ' ') do |opts| + opts.banner = "Usage: #{File.basename($0)} " \ + "[ruby options] [unicorn options] [rackup config file]" + + opts.separator "Ruby options:" + + lineno = 1 + opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line| + eval line, TOPLEVEL_BINDING, "-e", lineno + lineno += 1 + end + + opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do + $DEBUG = true + end + + opts.on("-w", "--warn", "turn warnings on for your script") do + $-w = true + end + + opts.on("-I", "--include PATH", + "specify $LOAD_PATH (may be used more than once)") do |path| + $LOAD_PATH.unshift(*path.split(/:/)) + end + + opts.on("-r", "--require LIBRARY", + "require the library, before executing your script") do |library| + require library + end + + opts.separator "Unicorn options:" + + # some of these switches exist for rackup command-line compatibility, + + opts.on("-o", "--host HOST", + "listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h| + host = h + set_listener = true + end + + opts.on("-p", "--port PORT", + "use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |p| + port = p.to_i + set_listener = true + end + + opts.on("-E", "--env ENVIRONMENT", + "use ENVIRONMENT for defaults (default: development)") do |e| + env = e + end + + opts.on("-D", "--daemonize", "run daemonized in the background") do |d| + daemonize = d ? true : false + end + + opts.on("-P", "--pid FILE", "DEPRECATED") do |f| + warn %q{Use of --pid/-P is strongly discouraged} + warn %q{Use the 'pid' directive in the Unicorn config file instead} + options[:pid] = File.expand_path(f) + end + + opts.on("-s", "--server SERVER", + "this flag only exists for compatibility") do |s| + warn "-s/--server only exists for compatibility with rackup" + end + + # Unicorn-specific stuff + opts.on("-l", "--listen {HOST:PORT|PATH}", + "listen on HOST:PORT or PATH", + "this may be specified multiple times", + "(default: #{Unicorn::Const::DEFAULT_LISTEN})") do |address| + listeners << address + end + + opts.on("-c", "--config-file FILE", "Unicorn-specific config file") do |f| + options[:config_file] = File.expand_path(f) + end + + # I'm avoiding Unicorn-specific config options on the command-line. + # IMNSHO, config options on the command-line are redundant given + # config files and make things unnecessarily complicated with multiple + # places to look for a config option. + + opts.separator "Common options:" + + opts.on_tail("-h", "--help", "Show this message") do + puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '') + exit + end + + opts.on_tail("-v", "--version", "Show version") do + puts "unicorn v#{Unicorn::Const::UNICORN_VERSION}" + exit + end + + opts.parse! ARGV +end + +config = ARGV[0] || "config.ru" +abort "configuration file #{config} not found" unless File.exist?(config) + +if config =~ /\.ru$/ + # parse embedded command-line options in config.ru comments + if File.open(config, "rb") { |fp| fp.sysread(fp.stat.size) } =~ /^#\\(.*)/ + opts.parse! $1.split(/\s+/) + end +end + +require 'pp' if $DEBUG + +app = lambda do || + # require Rack as late as possible in case $LOAD_PATH is modified + # in config.ru or command-line + inner_app = case config + when /\.ru$/ + raw = File.open(config, "rb") { |fp| fp.sysread(fp.stat.size) } + raw.sub!(/^__END__\n.*/, '') + eval("Rack::Builder.new {(#{raw}\n)}.to_app", nil, config) + else + require config + Object.const_get(File.basename(config, '.rb').capitalize) + end + pp({ :inner_app => inner_app }) if $DEBUG + case env + when "development" + Rack::Builder.new do + use Rack::CommonLogger, $stderr + use Rack::ShowExceptions + use Rack::Lint + run inner_app + end.to_app + when "deployment" + Rack::Builder.new do + use Rack::CommonLogger, $stderr + run inner_app + end.to_app + else + inner_app + end +end + +listeners << "#{host}:#{port}" if set_listener + +if $DEBUG + pp({ + :unicorn_options => options, + :app => app, + :daemonize => daemonize, + }) +end + +Unicorn::Launcher.daemonize! if daemonize +Rainbows.run(app, options) diff --git a/lib/rainbows.rb b/lib/rainbows.rb new file mode 100644 index 0000000..cb08ca3 --- /dev/null +++ b/lib/rainbows.rb @@ -0,0 +1,21 @@ +# -*- encoding: binary -*- +require 'unicorn' + +module Rainbows + + require 'rainbows/const' + require 'rainbows/configurator' + require 'rainbows/http_server' + require 'rainbows/http_response' + + autoload :Revactor, 'rainbows/revactor' + autoload :ThreadBase, 'rainbows/thread_base' + autoload :ThreadPool, 'rainbows/thread_pool' + + class << self + def run(app, options = {}) + HttpServer.new(app, options).start.join + end + end + +end diff --git a/lib/rainbows/configurator.rb b/lib/rainbows/configurator.rb new file mode 100644 index 0000000..449cdd9 --- /dev/null +++ b/lib/rainbows/configurator.rb @@ -0,0 +1,25 @@ +require 'rainbows' +module Rainbows + + class Configurator < ::Unicorn::Configurator + + def use(model) + begin + model = Rainbows.const_get(model) + rescue NameError + raise ArgumentError, "concurrency model #{model.inspect} not supported" + end + + Module === model or + raise ArgumentError, "concurrency model #{model.inspect} not supported" + set[:use] = model + end + + def worker_connections(nr) + (Integer === nr && nr > 0) || nr.nil? or + raise ArgumentError, "worker_connections must be an Integer or nil" + end + + end + +end diff --git a/lib/rainbows/const.rb b/lib/rainbows/const.rb new file mode 100644 index 0000000..9606b80 --- /dev/null +++ b/lib/rainbows/const.rb @@ -0,0 +1,22 @@ +module Rainbows + + module Const + RAINBOWS_VERSION = '0.93.0' + + include Unicorn::Const + + RACK_DEFAULTS = ::Unicorn::HttpRequest::DEFAULTS.merge({ + + # we need to observe many of the rules for thread-safety even + # with Revactor or Rev, so we're considered multithread-ed even + # when we're not technically... + "rack.multithread" => true, + "SERVER_SOFTWARE" => "Rainbows #{RAINBOWS_VERSION}", + }) + + CONN_CLOSE = "Connection: close\r\n" + CONN_ALIVE = "Connection: keep-alive\r\n" + LOCALHOST = "127.0.0.1" + + end +end diff --git a/lib/rainbows/http_response.rb b/lib/rainbows/http_response.rb new file mode 100644 index 0000000..ebaa4e7 --- /dev/null +++ b/lib/rainbows/http_response.rb @@ -0,0 +1,35 @@ +# -*- encoding: binary -*- +require 'time' +require 'rainbows' + +module Rainbows + + class HttpResponse < ::Unicorn::HttpResponse + + def self.write(socket, rack_response, out = []) + status, headers, body = rack_response + + if Array === out + status = CODES[status.to_i] || status + + headers.each do |key, value| + next if SKIP.include?(key.downcase) + if value =~ /\n/ + out.concat(value.split(/\n/).map! { |v| "#{key}: #{v}\r\n" }) + else + out << "#{key}: #{value}\r\n" + end + end + + socket.write("HTTP/1.1 #{status}\r\n" \ + "Date: #{Time.now.httpdate}\r\n" \ + "Status: #{status}\r\n" \ + "#{out.join('')}\r\n") + end + + body.each { |chunk| socket.write(chunk) } + ensure + body.respond_to?(:close) and body.close rescue nil + end + end +end diff --git a/lib/rainbows/http_server.rb b/lib/rainbows/http_server.rb new file mode 100644 index 0000000..355f3c5 --- /dev/null +++ b/lib/rainbows/http_server.rb @@ -0,0 +1,34 @@ +# -*- encoding: binary -*- +require 'rainbows' +module Rainbows + + class HttpServer < ::Unicorn::HttpServer + include Rainbows + + attr_accessor :worker_connections + attr_reader :use + + def initialize(app, options) + self.app = app + self.reexec_pid = 0 + self.init_listeners = options[:listeners] ? options[:listeners].dup : [] + self.config = Configurator.new(options.merge(:use_defaults => true)) + self.listener_opts = {} + config.commit!(self, :skip => [:listeners, :pid]) + + defined?(@use) or + self.use = Rainbows.const_get(:ThreadPool) + defined?(@worker_connections) or + @worker_connections = 4 + + #self.orig_app = app + end + + def use=(model) + (class << self; self; end).instance_eval { include model } + @use = model + end + + end + +end diff --git a/lib/rainbows/revactor.rb b/lib/rainbows/revactor.rb new file mode 100644 index 0000000..4c04079 --- /dev/null +++ b/lib/rainbows/revactor.rb @@ -0,0 +1,115 @@ +require 'rainbows' +require 'revactor' + +module Rainbows + + module Revactor + require 'rainbows/revactor/tee_input' + + include Unicorn + include Rainbows::Const + HttpServer.constants.each { |x| const_set(x, HttpServer.const_get(x)) } + + # once a client is accepted, it is processed in its entirety here + # in 3 easy steps: read request, call app, write app response + def process_client(client) + buf = client.read or return # this probably does not happen... + hp = HttpParser.new + env = {} + remote_addr = client.remote_addr + + begin + while ! hp.headers(env, buf) + buf << client.read + end + + env[Const::RACK_INPUT] = 0 == hp.content_length ? + HttpRequest::NULL_IO : + Rainbows::Revactor::TeeInput.new(client, env, hp, buf) + env[Const::REMOTE_ADDR] = remote_addr + response = app.call(env.update(RACK_DEFAULTS)) + + if 100 == response.first.to_i + client.write(Const::EXPECT_100_RESPONSE) + env.delete(Const::HTTP_EXPECT) + response = app.call(env) + end + + out = [ hp.keepalive? ? CONN_ALIVE : CONN_CLOSE ] if hp.headers? + HttpResponse.write(client, response, out) + end while hp.keepalive? and hp.reset.nil? and env.clear + client.close + # if we get any error, try to write something back to the client + # assuming we haven't closed the socket, but don't get hung up + # if the socket is already closed or broken. We'll always ensure + # the socket is closed at the end of this function + rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF + emergency_response(client, Const::ERROR_500_RESPONSE) + rescue HttpParserError # try to tell the client they're bad + buf.empty? or emergency_response(client, Const::ERROR_400_RESPONSE) + rescue Object => e + emergency_response(client, Const::ERROR_500_RESPONSE) + logger.error "Read error: #{e.inspect}" + logger.error e.backtrace.join("\n") + end + + # runs inside each forked worker, this sits around and waits + # for connections and doesn't die until the parent dies (or is + # given a INT, QUIT, or TERM signal) + def worker_loop(worker) + ppid = master_pid + init_worker_process(worker) + alive = worker.tmp # tmp is our lifeline to the master process + + trap(:USR1) { reopen_worker_logs(worker.nr) } + trap(:QUIT) { alive = false; LISTENERS.each { |s| s.close rescue nil } } + [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown + + Actor.current.trap_exit = true + + listeners = LISTENERS.map do |s| + TCPServer === s ? ::Revactor::TCP.listen(s, nil) : nil + end.compact + + logger.info "worker=#{worker.nr} ready with Revactor" + clients = [] + + listeners.map! do |s| + Actor.spawn(s) do |l| + begin + clients << Actor.spawn(l.accept) { |c| process_client(c) } + rescue Errno::EAGAIN, Errno::ECONNABORTED + rescue Object => e + if alive + logger.error "Unhandled listen loop exception #{e.inspect}." + logger.error e.backtrace.join("\n") + end + end while alive + end + end + + nr = 0 + begin + Actor.sleep 1 + clients.delete_if { |c| c.dead? } + if alive + alive.chmod(nr = 0 == nr ? 1 : 0) + ppid == Process.ppid or alive = false + end + end while alive || ! clients.empty? + end + + private + + # write a response without caring if it went out or not + # This is in the case of untrappable errors + def emergency_response(client, response_str) + client.instance_eval do + # this is Revactor implementation dependent + @_io.write_nonblock(response_str) rescue nil + end + client.close rescue nil + end + + end +end diff --git a/lib/rainbows/revactor/tee_input.rb b/lib/rainbows/revactor/tee_input.rb new file mode 100644 index 0000000..92effb4 --- /dev/null +++ b/lib/rainbows/revactor/tee_input.rb @@ -0,0 +1,44 @@ +# -*- encoding: binary -*- +require 'rainbows/revactor' + +module Rainbows + module Revactor + + # acts like tee(1) on an input input to provide a input-like stream + # while providing rewindable semantics through a File/StringIO + # backing store. On the first pass, the input is only read on demand + # so your Rack application can use input notification (upload progress + # and like). This should fully conform to the Rack::InputWrapper + # specification on the public API. This class is intended to be a + # strict interpretation of Rack::InputWrapper functionality and will + # not support any deviations from it. + class TeeInput < ::Unicorn::TeeInput + + private + + # tees off a +length+ chunk of data from the input into the IO + # backing store as well as returning it. +dst+ must be specified. + # returns nil if reading from the input returns nil + def tee(length, dst) + unless parser.body_eof? + begin + if parser.filter_body(dst, buf << socket.read).nil? + @tmp.write(dst) + return dst + end + rescue EOFError + end + end + finalize_input + end + + def finalize_input + while parser.trailers(req, buf).nil? + buf << socket.read + end + self.socket = nil + end + + end + end +end diff --git a/lib/rainbows/thread_base.rb b/lib/rainbows/thread_base.rb new file mode 100644 index 0000000..e544772 --- /dev/null +++ b/lib/rainbows/thread_base.rb @@ -0,0 +1,60 @@ + +module Rainbows + + module ThreadBase + + include Unicorn + include Rainbows::Const + + # write a response without caring if it went out or not + # This is in the case of untrappable errors + def emergency_response(client, response_str) + client.write_nonblock(response_str) rescue nil + client.close rescue nil + end + + # once a client is accepted, it is processed in its entirety here + # in 3 easy steps: read request, call app, write app response + def process_client(client) + buf = client.readpartial(CHUNK_SIZE) + hp = HttpParser.new + env = {} + remote_addr = TCPSocket === client ? client.peeraddr.last : LOCALHOST + + begin + while ! hp.headers(env, buf) + buf << client.readpartial(CHUNK_SIZE) + end + + env[RACK_INPUT] = 0 == hp.content_length ? + HttpRequest::NULL_IO : + Unicorn::TeeInput.new(client, env, hp, buf) + env[REMOTE_ADDR] = remote_addr + response = app.call(env.update(RACK_DEFAULTS)) + + if 100 == response.first.to_i + client.write(EXPECT_100_RESPONSE) + env.delete(HTTP_EXPECT) + response = app.call(env) + end + + out = [ hp.keepalive? ? CONN_ALIVE : CONN_CLOSE ] if hp.headers? + HttpResponse.write(client, response, out) + end while hp.keepalive? and hp.reset.nil? and env.clear + client.close + # if we get any error, try to write something back to the client + # assuming we haven't closed the socket, but don't get hung up + # if the socket is already closed or broken. We'll always ensure + # the socket is closed at the end of this function + rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF + emergency_response(client, ERROR_500_RESPONSE) + rescue HttpParserError # try to tell the client they're bad + buf.empty? or emergency_response(client, ERROR_400_RESPONSE) + rescue Object => e + emergency_response(client, ERROR_500_RESPONSE) + logger.error "Read error: #{e.inspect}" + logger.error e.backtrace.join("\n") + end + end +end + diff --git a/lib/rainbows/thread_pool.rb b/lib/rainbows/thread_pool.rb new file mode 100644 index 0000000..058205f --- /dev/null +++ b/lib/rainbows/thread_pool.rb @@ -0,0 +1,84 @@ +module Rainbows + + module ThreadPool + + include ThreadBase + + HttpServer.constants.each { |x| const_set(x, HttpServer.const_get(x)) } + + def worker_loop(worker) + init_worker_process(worker) + threads = ThreadGroup.new + alive = worker.tmp + nr = 0 + + # closing anything we IO.select on will raise EBADF + trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil } + trap(:QUIT) { alive = false; LISTENERS.map! { |s| s.close rescue nil } } + [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown + logger.info "worker=#{worker.nr} ready with ThreadPool" + + while alive && master_pid == Process.ppid + maintain_thread_count(threads) + threads.list.each do |thr| + alive.chmod(nr += 1) + thr.join(timeout / 2.0) and break + end + end + join_worker_threads(threads) + end + + def join_worker_threads(threads) + logger.info "Joining worker threads..." + t0 = Time.now + timeleft = timeout + threads.list.each { |thr| + thr.join(timeleft) + timeleft -= (Time.now - t0) + } + logger.info "Done joining worker threads." + end + + def maintain_thread_count(threads) + threads.list.each do |thr| + next if (Time.now - (thr[:t] || next)) < timeout + thr.kill! # take no prisoners for timeout violations + logger.error "killed #{thr.inspect} for being too old" + end + + while threads.list.size < worker_connections + threads.add(new_worker_thread) + end + end + + def new_worker_thread + Thread.new { + alive = true + thr = Thread.current + begin + ret = begin + thr[:t] = Time.now + IO.select(LISTENERS, nil, nil, timeout/2.0) or next + rescue Errno::EINTR + retry + rescue Errno::EBADF + return + end + ret.first.each do |sock| + begin + process_client(sock.accept_nonblock) + thr[:t] = Time.now + rescue Errno::EAGAIN, Errno::ECONNABORTED + end + end + rescue Object => e + if alive + logger.error "Unhandled listen loop exception #{e.inspect}." + logger.error e.backtrace.join("\n") + end + end while alive = LISTENERS.first + } + end + + end +end diff --git a/local.mk.sample b/local.mk.sample new file mode 100644 index 0000000..86c0e80 --- /dev/null +++ b/local.mk.sample @@ -0,0 +1,54 @@ +# this is the local.mk file used by Eric Wong on his dev boxes. +# GNUmakefile will source local.mk in the top-level source tree +# if it is present. +# +# This is depends on a bunch of GNU-isms from bash, sed, touch. + +DLEXT := so +rack_ver := 1.0.0 + +# Avoid loading rubygems to speed up tests because gmake is +# fork+exec heavy with Ruby. +ifeq ($(r19),) + ruby := $(HOME)/bin/ruby + RUBYLIB := $(HOME)/lib/ruby/gems/1.8/gems/rack-$(rack_ver)/lib +else + export PATH := $(HOME)/ruby-1.9/bin:$(PATH) + ruby := $(HOME)/ruby-1.9/bin/ruby --disable-gems + RUBYLIB := $(HOME)/ruby-1.9/lib/ruby/gems/1.9.1/gems/rack-$(rack_ver)/lib +endif + +# pipefail is THE reason to use bash (v3+) +SHELL := /bin/bash -e -o pipefail + +full-test: test-18 test-19 +test-18: + $(MAKE) test test-rails 2>&1 | sed -u -e 's!^!1.8 !' +test-19: + $(MAKE) test test-rails r19=1 2>&1 | sed -u -e 's!^!1.9 !' + +latest: NEWS + @awk 'BEGIN{RS="=== ";ORS=""}NR==2{sub(/\n$$/,"");print RS""$$0 }' < $< + +# publishes docs to http://rainbows.rubyforge.org +publish_doc: + -git set-file-times + $(RM) -r doc + $(MAKE) doc + $(MAKE) -s latest > doc/LATEST + find doc/images doc/js -type f | \ + TZ=UTC xargs touch -d '1970-01-01 00:00:00' doc/rdoc.css + $(MAKE) doc_gz + chmod 644 $$(find doc -type f) + rsync -av --delete doc/ \ + rubyforge.org:/var/www/gforge-projects/rainbows/ + git ls-files | xargs touch + +# Create gzip variants of the same timestamp as the original so nginx +# "gzip_static on" can serve the gzipped versions directly. +doc_gz: suf := html js css +doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.\(gif\|jpg\|png\|gz\)$$') +doc_gz: + touch doc/NEWS.atom.xml -d "$$(awk 'NR==1{print $$4,$$5,$$6}' NEWS)" + for i in $(docs); do \ + gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done diff --git a/rainbows.gemspec b/rainbows.gemspec new file mode 100644 index 0000000..b542553 --- /dev/null +++ b/rainbows.gemspec @@ -0,0 +1,47 @@ +# -*- encoding: binary -*- + +ENV["VERSION"] or abort "VERSION= must be specified" +manifest = File.readlines('.manifest').map! { |x| x.chomp! } + +# don't bother with tests that fork, not worth our time to get working +# with `gem check -t` ... (of course we care for them when testing with +# GNU make when they can run in parallel) +test_files = manifest.grep(%r{\Atest/unit/test_.*\.rb\z}).map do |f| + File.readlines(f).grep(/\bfork\b/).empty? ? f : nil +end.compact + +Gem::Specification.new do |s| + s.name = %q{rainbows} + s.version = ENV["VERSION"] + + s.authors = ["Eric Wong"] + s.date = Time.now.utc.strftime('%Y-%m-%d') + s.description = File.read("README").split(/\n\n/)[1] + s.email = %q{rainbows-talk@rubyforge.org} + s.executables = %w(rainbows) + + s.extra_rdoc_files = File.readlines('.document').map! do |x| + x.chomp! + if File.directory?(x) + manifest.grep(%r{\A#{x}/}) + elsif File.file?(x) + x + else + nil + end + end.flatten.compact + + s.files = manifest + s.homepage = %q{http://rainbows.rubyforge.org/} + s.summary = %q{Unicorn for Comet and slow clients} + s.rdoc_options = [ "-Na", "-t", "Rainbows #{s.summary}" ] + s.require_paths = %w(lib) + s.rubyforge_project = %q{rainbows} + + s.test_files = test_files + + s.add_dependency(%q) + s.add_dependency(%q, ["~> 0.93.1"]) + + # s.licenses = %w(GPLv2 Ruby) # accessor not compatible with older Rubygems +end -- cgit v1.2.3-24-ge0c7