heroku-forward 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2012 Daniel Doubrovkine, Art.sy Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Heroku::Forward [![Build Status](https://travis-ci.org/dblock/heroku-forward.png?branch=master)](https://travis-ci.org/dblock/heroku-forward)
2
+ ===============
3
+
4
+ Beat Heroku's 60 seconds timeout with a forward proxy.
5
+
6
+ What's this?
7
+ ------------
8
+
9
+ [Heroku](http://www.heroku.com/) will report an application crashing and yield an `R10 Boot Timeout` error when a web process took longer than 60 seconds to bind to its assigned `$PORT`. This error is often caused by a process being unable to reach an external resource, such as a database or because Heroku is pretty slow and you have a lot of gems in your `Gemfile`.
10
+
11
+ This gem implements a forward proxy using [em-proxy](https://github.com/igrigorik/em-proxy). This proxy is booted almost immediately, binding to the port assigned by Heroku. Heroku now reports the dyno up. The proxy then spawns your application's web server and establishes a connection over a unix domain socket (a file) between the proxy and the application. Once the application is ready, it will be able to serve HTTP requests normally. Until then requests may queue and some may timeout depending on how long it actually takes to start.
12
+
13
+ Usage
14
+ -----
15
+
16
+ Add `heroku-forward` and `em-proxy` to your `Gemfile`. Curently requires HEAD of `em-proxy` because of [this pull request](https://github.com/igrigorik/em-proxy/pull/31).
17
+
18
+ ``` ruby
19
+ gem "heroku-forward", :git => "https://github.com/dblock/heroku-forward.git"
20
+ gem "em-proxy", :git => "https://github.com/igrigorik/em-proxy.git"
21
+ ```
22
+
23
+ Create an application rackup file, eg. `my_app.ru` that boots your application. Under Rails, this is the file that calls `run`.
24
+
25
+ ``` ruby
26
+ require ::File.expand_path('../config/environment', __FILE__)
27
+ run MyApp::Application
28
+ ```
29
+
30
+ Modify your rackup file as follows. Under Rails this file is called `config.ru`.
31
+
32
+ ``` ruby
33
+ require 'rubygems'
34
+ require 'bundler'
35
+
36
+ $stdout.sync = true
37
+ Bundler.require(:rack)
38
+
39
+ port = (ARGV.first || ENV['PORT'] || 3000).to_i
40
+ env = ENV['RACK_ENV'] || 'development'
41
+
42
+ require 'em-proxy'
43
+ require 'logger'
44
+ require 'heroku-forward'
45
+
46
+ application = File.expand_path('../my_app.ru', __FILE__)
47
+ backend = Heroku::Forward::Backends::Thin.new(application: application, env: env)
48
+ proxy = Heroku::Forward::Proxy::Server.new(backend, { host: '0.0.0.0', port: port })
49
+ proxy.logger = Logger.new(STDOUT)
50
+ proxy.forward!
51
+ ```
52
+
53
+ This sets up a proxy on the port requested by Heroku and runs your application with Thin.
54
+
55
+ Foreman
56
+ -------
57
+
58
+ Heroku Cedar expects a `Procfile` that defines your application processes.
59
+
60
+ ```
61
+ web: bundle exec ruby config.ru
62
+ worker: bundle exec rake jobs:work
63
+ ```
64
+
65
+ You can use `foreman` to test the proxy locally with `foreman start web`.
66
+
67
+ Heroku Log
68
+ ----------
69
+
70
+ Here's the log output from an application that uses this gem. Notice that Heroku reports the state of `web.1` up after just 4 seconds, while the application takes 67 seconds to boot.
71
+
72
+ ```
73
+ 2012-12-11T23:33:42+00:00 heroku[web.1]: Starting process with command `bundle exec ruby config.ru`
74
+ 2012-12-11T23:33:46+00:00 app[web.1]: INFO -- : Launching Backend ...
75
+ 2012-12-11T23:33:46+00:00 app[web.1]: INFO -- : Launching Proxy Server at 0.0.0.0:42017 ...
76
+ 2012-12-11T23:33:46+00:00 app[web.1]: DEBUG -- : Attempting to connect to /tmp/thin20121211-2-1bfazzx.
77
+ 2012-12-11T23:33:46+00:00 app[web.1]: WARN -- : no connection, 10 retries left.
78
+ 2012-12-11T23:33:46+00:00 heroku[web.1]: State changed from starting to up
79
+ 2012-12-11T23:34:32+00:00 app[web.1]: >> Thin web server (v1.5.0 codename Knife)
80
+ 2012-12-11T23:34:32+00:00 app[web.1]: >> Maximum connections set to 1024
81
+ 2012-12-11T23:34:32+00:00 app[web.1]: >> Listening on /tmp/thin20121211-2-1bfazzx, CTRL+C to stop
82
+ 2012-12-11T23:34:53+00:00 app[web.1]: DEBUG -- : Attempting to connect to /tmp/thin20121211-2-1bfazzx.
83
+ 2012-12-11T23:34:53+00:00 app[web.1]: DEBUG -- : Proxy Server ready at 0.0.0.0:42017 (67s).
84
+ ```
85
+
86
+ Proxy Forwarding Options
87
+ ------------------------
88
+
89
+ `Heroku::Forward::Proxy::Server.forward!` accepts the following options:
90
+
91
+ * `delay`: number of seconds to sleep before launching the proxy, eg. `proxy.forward!(delay: 15)`. This prevents queuing of requests or reporting invalid `up` status to Heroku. It's recommended to set this value to as close as possible to the boot time of your application and less than the Heroku's 60s boot limit.
92
+
93
+ Fail-Safe
94
+ ---------
95
+
96
+ If you're worried about this implementation, consider building a fail-safe. Modify your `config.ru` to run without a proxy if `DISABLE_FORWARD_PROXY` is set as demonstrated in [this gist](https://gist.github.com/4263488). Add `DISABLE_FORWARD_PROXY` via `heroku config:add DISABLE_FORWARD_PROXY=1`.
97
+
98
+ Reading Materials
99
+ -----------------
100
+
101
+ * [Heroku R10 Boot Timeout](https://devcenter.heroku.com/articles/error-codes#r10-boot-timeout)
102
+ * [Beating Heroku's 60s Boot Times with the Cedar Stack and a Reverse Proxy](http://noverloop.be/beating-herokus-60s-boot-times-with-the-cedar-stack-and-a-reverse-proxy/) by Nicolas Overloop
103
+ * [Fighting the Unicorns: Becoming a Thin Wizard on Heroku](http://jgwmaxwell.com/fighting-the-unicorns-becoming-a-thin-wizard-on-heroku/) by JGW Maxwell
104
+ * [eventmachine](https://github.com/eventmachine/eventmachine)
105
+ * [em-proxy](https://github.com/igrigorik/em-proxy)
106
+
107
+ Contributing
108
+ ------------
109
+
110
+ Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
111
+
112
+ Copyright and License
113
+ ---------------------
114
+
115
+ MIT License, see [LICENSE](http://github.com/dblock/heroku-forward/raw/master/LICENSE.md) for details.
116
+
117
+ (c) 2012 [Daniel Doubrovkine](http://github.com/dblock), [Art.sy](http://artsy.github.com)
@@ -0,0 +1,10 @@
1
+ require 'i18n'
2
+ require 'tempfile'
3
+ require 'em-proxy'
4
+ require 'posix/spawn'
5
+
6
+ I18n.load_path << File.join(File.dirname(__FILE__), "heroku", "forward", "config", "locales", "en.yml")
7
+
8
+ require 'heroku/forward'
9
+
10
+
@@ -0,0 +1,4 @@
1
+ require 'heroku/forward/version'
2
+ require 'heroku/forward/errors'
3
+ require 'heroku/forward/backends'
4
+ require 'heroku/forward/proxy'
@@ -0,0 +1 @@
1
+ require 'heroku/forward/backends/thin'
@@ -0,0 +1,56 @@
1
+ module Heroku
2
+ module Forward
3
+ module Backends
4
+ class Thin
5
+ include POSIX::Spawn
6
+
7
+ attr_accessor :application
8
+ attr_accessor :socket
9
+ attr_accessor :environment
10
+ attr_accessor :pid
11
+
12
+ # options:
13
+ # application: passed with -R, eg. app.ru
14
+ # socket: passed with --socket, eg. /tmp/thin.sock
15
+ # env: passed with -e, defaults to 'development'
16
+ def initialize(options = {})
17
+ @application = options[:application]
18
+ @socket = options[:socket] || new_socket
19
+ @env = options[:env] || :development
20
+ end
21
+
22
+ def spawn!
23
+ return false if spawned?
24
+ check!
25
+ @pid = spawn("thin start -R #{@application} --socket #{@socket} -e #{@env}")
26
+ @spawned = true
27
+ end
28
+
29
+ def terminate!
30
+ return false unless spawned?
31
+ Process.kill 'QUIT', @pid
32
+ @spawned = false
33
+ true
34
+ end
35
+
36
+ def spawned?
37
+ !! @spawned
38
+ end
39
+
40
+ private
41
+
42
+ def new_socket
43
+ Tempfile.open 'thin' do |file|
44
+ return file.path
45
+ end
46
+ end
47
+
48
+ def check!
49
+ raise Heroku::Forward::Errors::MissingBackendOptionError.new('application') unless @application && @application.length > 0
50
+ raise Heroku::Forward::Errors::MissingBackendApplicationError.new(@application) unless File.exists?(@application)
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,17 @@
1
+ en:
2
+ heroku:
3
+ forward:
4
+ errors:
5
+ messages:
6
+ missing_backend_option:
7
+ message: "Missing backend option: %{name}."
8
+ summary: "A required option was not provided to the back-end."
9
+ resolution: "Review the documentation for the back-end that you're trying to proxy to."
10
+ missing_backend_application:
11
+ message: "Missing backend application in '%{path}'."
12
+ summary: "The file supplied to the back-end in the 'application' option is invalid."
13
+ resolution: "This is typically a file called 'myapp.ru', for Rails applications it will contain a call to 'MyApp::Application.initialize!'."
14
+ backend_failed_to_start:
15
+ message: "The back-end failed to start in a timely fashion."
16
+ summary: "The proxy started, but failed to connect to the back-end after retrying multiple times."
17
+ resolution: "Check that your application can start without the proxy and serve HTTP requests."
@@ -0,0 +1,4 @@
1
+ require 'heroku/forward/errors/heroku_forward_error'
2
+ require 'heroku/forward/errors/missing_backend_option_error'
3
+ require 'heroku/forward/errors/missing_backend_application_error'
4
+ require 'heroku/forward/errors/backend_failed_to_start_error'
@@ -0,0 +1,11 @@
1
+ module Heroku
2
+ module Forward
3
+ module Errors
4
+ class BackendFailedToStartError < HerokuForwardError
5
+ def initialize
6
+ super(compose_message("backend_failed_to_start"))
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+ module Heroku #:nodoc:
3
+ module Forward
4
+ module Errors #:nodoc:
5
+
6
+ # Default parent error for all custom errors. This handles the base
7
+ # key for the translations and provides the convenience method for
8
+ # translating the messages.
9
+ #
10
+ # Generously borrowed from Mongoid[https://github.com/mongoid/mongoid/blob/master/lib/mongoid/errors/mongoid_error.rb].
11
+ class HerokuForwardError < StandardError
12
+
13
+ # Problem occurred.
14
+ attr_reader :problem
15
+
16
+ # Summary of the problem.
17
+ attr_reader :summary
18
+
19
+ # Suggested problem resolution.
20
+ attr_reader :resolution
21
+
22
+ # Compose the message.
23
+ # === Parameters
24
+ # [key] Lookup key in the translation table.
25
+ # [attributes] The objects to pass to create the message.
26
+ def compose_message(key, attributes = {})
27
+ @problem = create_problem(key, attributes)
28
+ @summary = create_summary(key, attributes)
29
+ @resolution = create_resolution(key, attributes)
30
+
31
+ "\nProblem:\n #{@problem}"+
32
+ "\nSummary:\n #{@summary}"+
33
+ "\nResolution:\n #{@resolution}"
34
+ end
35
+
36
+ private
37
+
38
+ BASE_KEY = "heroku.forward.errors.messages" #:nodoc:
39
+
40
+ # Given the key of the specific error and the options hash, translate the
41
+ # message.
42
+ #
43
+ # === Parameters
44
+ # [key] The key of the error in the locales.
45
+ # [options] The objects to pass to create the message.
46
+ #
47
+ # Returns a localized error message string.
48
+ def translate(key, options)
49
+ ::I18n.translate("#{BASE_KEY}.#{key}", { :locale => :en }.merge(options)).strip
50
+ end
51
+
52
+ # Create the problem.
53
+ #
54
+ # === Parameters
55
+ # [key] The error key.
56
+ # [attributes] The attributes to interpolate.
57
+ #
58
+ # Returns the problem.
59
+ def create_problem(key, attributes)
60
+ translate("#{key}.message", attributes)
61
+ end
62
+
63
+ # Create the summary.
64
+ #
65
+ # === Parameters
66
+ # [key] The error key.
67
+ # [attributes] The attributes to interpolate.
68
+ #
69
+ # Returns the summary.
70
+ def create_summary(key, attributes)
71
+ translate("#{key}.summary", attributes)
72
+ end
73
+
74
+ # Create the resolution.
75
+ #
76
+ # === Parameters
77
+ # [key] The error key.
78
+ # [attributes] The attributes to interpolate.
79
+ #
80
+ # Returns the resolution.
81
+ def create_resolution(key, attributes)
82
+ translate("#{key}.resolution", attributes)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,13 @@
1
+ module Heroku
2
+ module Forward
3
+ module Errors
4
+ class MissingBackendApplicationError < HerokuForwardError
5
+ def initialize(path)
6
+ super(compose_message("missing_backend_application", {
7
+ :path => path
8
+ }))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Heroku
2
+ module Forward
3
+ module Errors
4
+ class MissingBackendOptionError < HerokuForwardError
5
+ def initialize(name)
6
+ super(compose_message("missing_backend_option", {
7
+ :name => name
8
+ }))
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require 'heroku/forward/proxy/server'
@@ -0,0 +1,96 @@
1
+ module Heroku
2
+ module Forward
3
+ module Proxy
4
+
5
+ class Server
6
+ attr_reader :host, :port, :backend, :retries, :start
7
+ attr_accessor :logger
8
+
9
+ def initialize(backend, options = {})
10
+ @host = options[:host] || '0.0.0.0'
11
+ @port = options[:port] || 3000
12
+ @retries = options[:retries] || 10
13
+ @backend = backend
14
+ end
15
+
16
+ def on_connect(&callback)
17
+ if block_given?
18
+ @on_connect = callback
19
+ elsif @on_connect
20
+ @on_connect.call
21
+ end
22
+ end
23
+
24
+ def forward!(options = {})
25
+
26
+ @start = Time.now
27
+
28
+ logger.info "Launching Backend ..." if logger
29
+
30
+ backend.spawn!
31
+
32
+ if options[:delay] && (delay = options[:delay].to_i) > 0
33
+ logger.info "Waiting #{delay}s to Launch Proxy Server ..." if logger
34
+ sleep delay
35
+ end
36
+
37
+ logger.info "Launching Proxy Server at #{host}:#{port} ..." if logger
38
+
39
+ s = self
40
+ ::Proxy.start({ :host => host, :port => port, :debug => false }) do |conn|
41
+ if @start
42
+ EM.next_tick do
43
+ s.send(:connect, conn)
44
+ end
45
+ else
46
+ s.send(:connect, conn)
47
+ end
48
+
49
+ conn.on_connect do
50
+ s.on_connect
51
+ end
52
+
53
+ conn.on_data do |data|
54
+ data
55
+ end
56
+
57
+ conn.on_response do |backend, resp|
58
+ resp
59
+ end
60
+
61
+ conn.on_finish do
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ def stop!
68
+ logger.info "Terminating Proxy Server" if logger
69
+ EventMachine.stop
70
+ logger.info "Terminating Web Server" if logger
71
+ backend.terminate!
72
+ end
73
+
74
+ private
75
+
76
+ def connect(conn)
77
+ begin
78
+ if start
79
+ logger.debug "Attempting to connect to #{backend.socket}." if logger
80
+ end
81
+ conn.server backend, :socket => backend.socket
82
+ if @start
83
+ logger.debug "Proxy Server ready at #{host}:#{port} (#{(Time.now - start).to_i}s)." if logger
84
+ @start = nil
85
+ end
86
+ rescue RuntimeError => e
87
+ raise BackendFailedToStartError.new if @retries <= 0
88
+ logger.warn "#{e.message}, #{retries} #{retries == 1 ? 'retry' : 'retries'} left." if logger
89
+ @retries -= 1
90
+ end
91
+ end
92
+
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ module Heroku
2
+ module Forward
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,209 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroku-forward
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Doubrovkine
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: em-proxy
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.8
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.1.8
30
+ - !ruby/object:Gem::Dependency
31
+ name: i18n
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.6'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.6'
46
+ - !ruby/object:Gem::Dependency
47
+ name: posix-spawn
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '10.0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '10.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: bundler
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '2.6'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '2.6'
110
+ - !ruby/object:Gem::Dependency
111
+ name: jeweler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '1.6'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '1.6'
126
+ - !ruby/object:Gem::Dependency
127
+ name: thin
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '1.5'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '1.5'
142
+ - !ruby/object:Gem::Dependency
143
+ name: em-http-request
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: '1.0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: '1.0'
158
+ description:
159
+ email: dblock@dblock.org
160
+ executables: []
161
+ extensions: []
162
+ extra_rdoc_files:
163
+ - LICENSE.md
164
+ - README.md
165
+ files:
166
+ - lib/heroku-forward.rb
167
+ - lib/heroku/forward.rb
168
+ - lib/heroku/forward/backends.rb
169
+ - lib/heroku/forward/backends/thin.rb
170
+ - lib/heroku/forward/config/locales/en.yml
171
+ - lib/heroku/forward/errors.rb
172
+ - lib/heroku/forward/errors/backend_failed_to_start_error.rb
173
+ - lib/heroku/forward/errors/heroku_forward_error.rb
174
+ - lib/heroku/forward/errors/missing_backend_application_error.rb
175
+ - lib/heroku/forward/errors/missing_backend_option_error.rb
176
+ - lib/heroku/forward/proxy.rb
177
+ - lib/heroku/forward/proxy/server.rb
178
+ - lib/heroku/forward/version.rb
179
+ - LICENSE.md
180
+ - README.md
181
+ homepage: http://github.com/dblock/heroku-forward
182
+ licenses:
183
+ - MIT
184
+ post_install_message:
185
+ rdoc_options: []
186
+ require_paths:
187
+ - lib
188
+ required_ruby_version: !ruby/object:Gem::Requirement
189
+ none: false
190
+ requirements:
191
+ - - ! '>='
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ segments:
195
+ - 0
196
+ hash: 394519437
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ none: false
199
+ requirements:
200
+ - - ! '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 1.8.24
206
+ signing_key:
207
+ specification_version: 3
208
+ summary: Beat Heroku's 60s boot timeout with a forward proxy.
209
+ test_files: []