rainbows 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/FAQ CHANGED
@@ -48,3 +48,42 @@ It depends on the size and amount of static files you're serving. If
48
48
  you're serving a lot of static files (especially large ones), then by
49
49
  all means use nginx. If not, then \Rainbows! is likely a "good enough"
50
50
  solution even if nginx will always outperform it in raw throughput.
51
+
52
+
53
+ === How do I support SSL?
54
+
55
+ If you need a streaming "rack.input" to do upload processing within your
56
+ Rack application, then {stunnel}[http://stunnel.org/] is required.
57
+ Otherwise, nginx is a perfectly good reverse proxy.
58
+
59
+ Refer to the {Unicorn FAQ}[http://unicorn.bogomips.org/FAQ.html] on how
60
+ to ensure redirects go to "https://" URLs.
61
+
62
+
63
+ === Is there a "rainbows_rails" command like there is "unicorn_rails"?
64
+
65
+ Only if you write one and plan to support it.
66
+
67
+ "unicorn_rails" was written primarily to support older versions of
68
+ Rails. Since \Rainbows! is designed for newer Rails, it can just use
69
+ a "config.ru" file like other Rack frameworks and applications.
70
+
71
+ For Rails 2.3.x and later, the following config.ru will work for you:
72
+
73
+ ENV["RAILS_ENV"] ||= ENV["RACK_ENV"]
74
+ require "config/environment"
75
+ use Rails::Rack::Static
76
+ run ActionController::Dispatcher.new
77
+
78
+ For older versions of Rails, the following config.ru will work:
79
+
80
+ ENV["RAILS_ENV"] ||= ENV["RACK_ENV"]
81
+ require 'config/boot'
82
+ require 'config/environment'
83
+ require 'unicorn/app/old_rails'
84
+ require 'unicorn/app/old_rails/static' # not needed with Unicorn 0.95+
85
+ use Unicorn::App::OldRails::Static
86
+ run Unicorn::App::OldRails.new
87
+
88
+ One thing to watch out for is that RAILS_ENV will not be set in the
89
+ environment for you, thus we set it to match RACK_ENV.
data/GIT-VERSION-GEN CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
3
  GVF=GIT-VERSION-FILE
4
- DEF_VER=v0.5.0.GIT
4
+ DEF_VER=v0.6.0.GIT
5
5
 
6
6
  LF='
7
7
  '
data/GNUmakefile CHANGED
@@ -64,7 +64,7 @@ NEWS: GIT-VERSION-FILE
64
64
  $(rake) -s news_rdoc > $@+
65
65
  mv $@+ $@
66
66
 
67
- SINCE = 0.4.0
67
+ SINCE = 0.5.0
68
68
  ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..)
69
69
  ChangeLog: GIT-VERSION-FILE
70
70
  @echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+
data/Rakefile CHANGED
@@ -24,6 +24,7 @@ def tags
24
24
  end
25
25
 
26
26
  cgit_url = "http://git.bogomips.org/cgit/rainbows.git"
27
+ git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/rainbows.git'
27
28
 
28
29
  desc 'prints news as an Atom feed'
29
30
  task :news_atom do
@@ -88,8 +89,6 @@ desc "print release notes for Rubyforge"
88
89
  task :release_notes do
89
90
  require 'rubygems'
90
91
 
91
- git_url = ENV['GIT_URL'] || 'git://git.bogomips.org/rainbows.git'
92
-
93
92
  spec = Gem::Specification.load('rainbows.gemspec')
94
93
  puts spec.description.strip
95
94
  puts ""
@@ -117,3 +116,41 @@ task :publish_news do
117
116
  rf.login
118
117
  rf.post_news('rainbows', subject, body)
119
118
  end
119
+
120
+ desc "post to RAA"
121
+ task :raa_update do
122
+ require 'rubygems'
123
+ require 'net/http'
124
+ require 'net/netrc'
125
+ rc = Net::Netrc.locate('rainbows-raa') or abort "~/.netrc not found"
126
+ password = rc.password
127
+
128
+ s = Gem::Specification.load('rainbows.gemspec')
129
+ desc = [ s.description.strip ]
130
+ desc << ""
131
+ desc << "* #{s.email}"
132
+ desc << "* #{git_url}"
133
+ desc << "* #{cgit_url}"
134
+ desc = desc.join("\n")
135
+ uri = URI.parse('http://raa.ruby-lang.org/regist.rhtml')
136
+ form = {
137
+ :name => s.name,
138
+ :short_description => s.summary,
139
+ :version => s.version.to_s,
140
+ :status => 'experimental',
141
+ :owner => s.authors.first,
142
+ :email => s.email,
143
+ :category_major => 'Library',
144
+ :category_minor => 'Web',
145
+ :url => s.homepage,
146
+ :download => "http://rubyforge.org/frs/?group_id=8977",
147
+ :license => "Ruby's",
148
+ :description_style => 'Plain',
149
+ :description => desc,
150
+ :pass => password,
151
+ :submit => "Update",
152
+ }
153
+ res = Net::HTTP.post_form(uri, form)
154
+ p res
155
+ puts res.body
156
+ end
data/lib/rainbows.rb CHANGED
@@ -5,9 +5,20 @@ module Rainbows
5
5
 
6
6
  # global vars because class/instance variables are confusing me :<
7
7
  # this struct is only accessed inside workers and thus private to each
8
- G = Struct.new(:cur, :max, :logger, :alive, :app).new
9
8
  # G.cur may not be used the network concurrency model
10
- G.alive = true
9
+ class State < Struct.new(:alive,:m,:cur,:server,:tmp)
10
+ def tick
11
+ tmp.chmod(self.m = m == 0 ? 1 : 0)
12
+ alive && server.master_pid == Process.ppid or quit!
13
+ end
14
+
15
+ def quit!
16
+ self.alive = false
17
+ server.class.const_get(:LISTENERS).map! { |s| s.close rescue nil }
18
+ false
19
+ end
20
+ end
21
+ G = State.new(true, 0, 0)
11
22
 
12
23
  require 'rainbows/const'
13
24
  require 'rainbows/http_server'
@@ -56,6 +67,7 @@ module Rainbows
56
67
  :ThreadSpawn => 30,
57
68
  :ThreadPool => 10,
58
69
  :Rev => 50,
70
+ :RevThreadSpawn => 50,
59
71
  :EventMachine => 50,
60
72
  }.each do |model, _|
61
73
  u = model.to_s.gsub(/([a-z0-9])([A-Z0-9])/) { "#{$1}_#{$2.downcase!}" }
data/lib/rainbows/base.rb CHANGED
@@ -10,14 +10,6 @@ module Rainbows
10
10
  include Rainbows::Const
11
11
  G = Rainbows::G
12
12
 
13
- # write a response without caring if it went out or not for error
14
- # messages.
15
- # TODO: merge into Unicorn::HttpServer
16
- def emergency_response(client, response_str)
17
- client.write_nonblock(response_str) rescue nil
18
- client.close rescue nil
19
- end
20
-
21
13
  def listen_loop_error(e)
22
14
  G.alive or return
23
15
  logger.error "Unhandled listen loop exception #{e.inspect}."
@@ -26,20 +18,13 @@ module Rainbows
26
18
 
27
19
  def init_worker_process(worker)
28
20
  super(worker)
29
- G.cur = 0
30
- G.max = worker_connections
31
- G.logger = logger
32
- G.app = app
21
+ G.tmp = worker.tmp
33
22
 
34
23
  # we're don't use the self-pipe mechanism in the Rainbows! worker
35
24
  # since we don't defer reopening logs
36
25
  HttpServer::SELF_PIPE.each { |x| x.close }.clear
37
- trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil }
38
- trap(:QUIT) do
39
- G.alive = false
40
- # closing anything we IO.select on will raise EBADF
41
- HttpServer::LISTENERS.map! { |s| s.close rescue nil }
42
- end
26
+ trap(:USR1) { reopen_worker_logs(worker.nr) }
27
+ trap(:QUIT) { G.quit! }
43
28
  [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
44
29
  logger.info "Rainbows! #@use worker_connections=#@worker_connections"
45
30
  end
@@ -79,23 +64,16 @@ module Rainbows
79
64
  # assuming we haven't closed the socket, but don't get hung up
80
65
  # if the socket is already closed or broken. We'll always ensure
81
66
  # the socket is closed at the end of this function
82
- rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
83
- emergency_response(client, ERROR_500_RESPONSE)
84
- rescue HttpParserError # try to tell the client they're bad
85
- buf.empty? or emergency_response(client, ERROR_400_RESPONSE)
86
- rescue Object => e
87
- emergency_response(client, ERROR_500_RESPONSE)
88
- logger.error "Read error: #{e.inspect}"
89
- logger.error e.backtrace.join("\n")
67
+ rescue => e
68
+ handle_error(client, e)
90
69
  end
91
70
 
92
- def join_threads(threads, worker)
93
- Rainbows::G.alive = false
71
+ def join_threads(threads)
72
+ G.quit!
94
73
  expire = Time.now + (timeout * 2.0)
95
- m = 0
96
74
  until (threads.delete_if { |thr| ! thr.alive? }).empty?
97
75
  threads.each { |thr|
98
- worker.tmp.chmod(m = 0 == m ? 1 : 0)
76
+ G.tick
99
77
  thr.join(1)
100
78
  break if Time.now >= expire
101
79
  }
@@ -3,7 +3,7 @@
3
3
  module Rainbows
4
4
 
5
5
  module Const
6
- RAINBOWS_VERSION = '0.5.0'
6
+ RAINBOWS_VERSION = '0.6.0'
7
7
 
8
8
  include Unicorn::Const
9
9
 
@@ -8,6 +8,10 @@ module Rainbows
8
8
  include Rainbows::Const
9
9
  G = Rainbows::G
10
10
 
11
+ def self.setup(klass)
12
+ klass.const_set(:APP, G.server.app)
13
+ end
14
+
11
15
  def post_init
12
16
  @remote_addr = ::TCPSocket === @_io ? @_io.peeraddr.last : LOCALHOST
13
17
  @env = {}
@@ -28,8 +32,8 @@ module Rainbows
28
32
  when HttpParserError # try to tell the client they're bad
29
33
  ERROR_400_RESPONSE
30
34
  else
31
- G.logger.error "Read error: #{e.inspect}"
32
- G.logger.error e.backtrace.join("\n")
35
+ G.server.logger.error "Read error: #{e.inspect}"
36
+ G.server.logger.error e.backtrace.join("\n")
33
37
  ERROR_500_RESPONSE
34
38
  end
35
39
  write(msg)
@@ -0,0 +1,80 @@
1
+ # -*- encoding: binary -*-
2
+ require 'thread' # for Queue
3
+ require 'rainbows/ev_core'
4
+
5
+ module Rainbows
6
+
7
+ # base module for mixed Thread + evented models like RevThreadSpawn
8
+ module EvThreadCore
9
+ include EvCore
10
+
11
+ def post_init
12
+ super
13
+ @lock = Mutex.new
14
+ @thread = nil
15
+ end
16
+
17
+ # we pass ourselves off as a Socket to Unicorn::TeeInput and this
18
+ # is the only method Unicorn::TeeInput requires from the socket
19
+ def readpartial(length, buf = "")
20
+ # we must modify the original buffer if there was one
21
+ length == 0 and return buf.replace("")
22
+
23
+ # wait on the main loop to feed us
24
+ while @tbuf.size == 0
25
+ @tbuf.write(@state.pop)
26
+ resume
27
+ end
28
+ buf.replace(@tbuf.read(length))
29
+ end
30
+
31
+ def app_spawn(input)
32
+ begin
33
+ @thread.nil? or @thread.join # only one thread per connection
34
+ env = @env.dup
35
+ alive, headers = @hp.keepalive?, @hp.headers?
36
+ @thread = Thread.new(self) do |client|
37
+ begin
38
+ env[REMOTE_ADDR] = @remote_addr
39
+ env[RACK_INPUT] = input || TeeInput.new(client, env, @hp, @buf)
40
+ response = APP.call(env.update(RACK_DEFAULTS))
41
+ if 100 == response.first.to_i
42
+ write(EXPECT_100_RESPONSE)
43
+ env.delete(HTTP_EXPECT)
44
+ response = APP.call(env)
45
+ end
46
+
47
+ alive &&= G.alive
48
+ out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if headers
49
+ response_write(response, out)
50
+ rescue => e
51
+ handle_error(e) rescue nil
52
+ end
53
+ end
54
+ if alive # in case we pipeline
55
+ @hp.reset
56
+ redo if @hp.headers(@env.clear, @buf)
57
+ end
58
+ end while false
59
+ end
60
+
61
+ def on_read(data)
62
+ case @state
63
+ when :headers
64
+ @hp.headers(@env, @buf << data) or return
65
+ if 0 == @hp.content_length
66
+ app_spawn(HttpRequest::NULL_IO) # common case
67
+ else # nil or len > 0
68
+ @state, @tbuf = Queue.new, ::IO::Buffer.new
69
+ app_spawn(nil)
70
+ end
71
+ when Queue
72
+ pause
73
+ @state << data
74
+ end
75
+ rescue => e
76
+ handle_error(e)
77
+ end
78
+
79
+ end
80
+ end
@@ -53,18 +53,17 @@ module Rainbows
53
53
  def app_call
54
54
  begin
55
55
  (@env[RACK_INPUT] = @input).rewind
56
- alive = @hp.keepalive?
57
56
  @env[REMOTE_ADDR] = @remote_addr
58
57
  @env[ASYNC_CALLBACK] = method(:response_write)
59
58
 
60
- response = catch(:async) { G.app.call(@env.update(RACK_DEFAULTS)) }
59
+ response = catch(:async) { APP.call(@env.update(RACK_DEFAULTS)) }
61
60
 
62
61
  # too tricky to support pipelining with :async since the
63
62
  # second (pipelined) request could be a stuck behind a
64
63
  # long-running async response
65
64
  (response.nil? || -1 == response.first) and return @state = :close
66
65
 
67
- alive &&= G.alive
66
+ alive = @hp.keepalive? && G.alive
68
67
  out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
69
68
  response_write(response, out, alive)
70
69
 
@@ -166,22 +165,17 @@ module Rainbows
166
165
 
167
166
  module Server
168
167
 
169
- def initialize(conns)
170
- @limit = Rainbows::G.max + HttpServer::LISTENERS.size
171
- @em_conns = conns
172
- end
173
-
174
168
  def close
175
169
  detach
176
170
  @io.close
177
171
  end
178
172
 
179
173
  def notify_readable
180
- return if @em_conns.size >= @limit
174
+ return if CUR.size >= MAX
181
175
  begin
182
176
  io = @io.accept_nonblock
183
177
  sig = EM.attach_fd(io.fileno, false)
184
- @em_conns[sig] = Client.new(sig, io)
178
+ CUR[sig] = Client.new(sig, io)
185
179
  rescue Errno::EAGAIN, Errno::ECONNABORTED
186
180
  end
187
181
  end
@@ -192,24 +186,26 @@ module Rainbows
192
186
  # given a INT, QUIT, or TERM signal)
193
187
  def worker_loop(worker)
194
188
  init_worker_process(worker)
195
- m = 0
196
189
 
197
190
  # enable them both, should be non-fatal if not supported
198
191
  EM.epoll
199
192
  EM.kqueue
200
193
  logger.info "EventMachine: epoll=#{EM.epoll?} kqueue=#{EM.kqueue?}"
194
+ Server.const_set(:MAX, G.server.worker_connections +
195
+ HttpServer::LISTENERS.size)
196
+ EvCore.setup(Client)
201
197
  EM.run {
202
198
  conns = EM.instance_variable_get(:@conns) or
203
199
  raise RuntimeError, "EM @conns instance variable not accessible!"
200
+ Server.const_set(:CUR, conns)
204
201
  EM.add_periodic_timer(1) do
205
- worker.tmp.chmod(m = 0 == m ? 1 : 0)
206
- unless G.alive
202
+ unless G.tick
207
203
  conns.each_value { |client| Client === client and client.quit }
208
204
  EM.stop if conns.empty? && EM.reactor_running?
209
205
  end
210
206
  end
211
207
  LISTENERS.map! do |s|
212
- EM.watch(s, Server, conns) { |c| c.notify_readable = true }
208
+ EM.watch(s, Server) { |c| c.notify_readable = true }
213
209
  end
214
210
  }
215
211
  end
@@ -30,7 +30,7 @@ module Rainbows
30
30
 
31
31
  body.each { |chunk| socket.write(chunk) }
32
32
  ensure
33
- body.respond_to?(:close) and body.close rescue nil
33
+ body.respond_to?(:close) and body.close
34
34
  end
35
35
  end
36
36
  end
@@ -5,21 +5,27 @@ module Rainbows
5
5
  class HttpServer < ::Unicorn::HttpServer
6
6
  include Rainbows
7
7
 
8
- @@instance = nil
9
-
10
8
  class << self
11
9
  def setup(block)
12
- @@instance.instance_eval(&block)
10
+ G.server.instance_eval(&block)
13
11
  end
14
12
  end
15
13
 
16
14
  def initialize(app, options)
17
- @@instance = self
15
+ G.server = self
18
16
  rv = super(app, options)
19
17
  defined?(@use) or use(:Base)
20
18
  @worker_connections ||= MODEL_WORKER_CONNECTIONS[@use]
21
19
  end
22
20
 
21
+ def reopen_worker_logs(worker_nr)
22
+ logger.info "worker=#{worker_nr} reopening logs..."
23
+ Unicorn::Util.reopen_logs
24
+ logger.info "worker=#{worker_nr} done reopening logs"
25
+ rescue
26
+ G.quit! # let the master reopen and refork us
27
+ end
28
+
23
29
  #:stopdoc:
24
30
  #
25
31
  # Add one second to the timeout since our fchmod heartbeat is less