require 'lib/em-proxy'
require 'ansi/code'
require 'uri'
# = Balancing Proxy
#
# A simple example of a balancing, reverse/forward proxy such as Nginx or HAProxy.
#
# Given a list of backends, it's able to distribute requests to backends
# via different strategies (_random_, _roundrobin_, _balanced_), see Backend.select.
#
# This example is provided for didactic purposes. Nevertheless, based on some preliminary benchmarks
# and live tests, it performs well in production usage.
#
# You can customize the behaviour of the proxy by changing the BalancingProxy::Callbacks
# callbacks. To give you some ideas:
#
# * Store statistics for the proxy load in Redis (eg.: $redis.incr "proxy>backends>#{backend}>total")
# * Use Redis' _SortedSet_ instead of updating the Backend.list hash to allow for polling from external process
# * Use em-websocket[https://github.com/igrigorik/em-websocket] to build a web frontend for monitoring
#
module BalancingProxy
extend self
BACKENDS = [
{:url => "http://0.0.0.0:3000"},
{:url => "http://0.0.0.0:3001"},
{:url => "http://0.0.0.0:3002"}
]
# Represents a "backend", ie. the endpoint for the proxy.
#
# This could be eg. a WEBrick webserver (see below), so the proxy server works as a _reverse_ proxy.
# But it could also be a proxy server, so the proxy server works as a _forward_ proxy.
#
class Backend
attr_reader :url, :host, :port
attr_accessor :load
alias :to_s :url
def initialize(options={})
raise ArgumentError, "Please provide a :url and :load" unless options[:url]
@url = options[:url]
@load = options[:load] || 0
parsed = URI.parse(@url)
@host, @port = parsed.host, parsed.port
end
# Select backend: balanced, round-robin or random
#
def self.select(strategy = :balanced)
@strategy = strategy.to_sym
case @strategy
when :balanced
backend = list.sort_by { |b| b.load }.first
when :roundrobin
@pool = list.clone if @pool.nil? || @pool.empty?
backend = @pool.shift
when :random
backend = list[ rand(list.size-1) ]
else
raise ArgumentError, "Unknown strategy: #{@strategy}"
end
Callbacks.on_select.call(backend)
yield backend if block_given?
backend
end
# List of backends
#
def self.list
@list ||= BACKENDS.map { |backend| new backend }
end
# Return balancing strategy
#
def self.strategy
@strategy
end
# Increment "currently serving requests" counter
#
def increment_counter
self.load += 1
end
# Decrement "currently serving requests" counter
#
def decrement_counter
self.load -= 1
end
end
# Callbacks for em-proxy events
#
module Callbacks
include ANSI::Code
extend self
def on_select
lambda do |backend|
puts black_on_white { 'on_select'.ljust(12) } + " #{backend.inspect}"
backend.increment_counter if Backend.strategy == :balanced
end
end
def on_connect
lambda do |backend|
puts black_on_magenta { 'on_connect'.ljust(12) } + ' ' + bold { backend }
end
end
def on_data
lambda do |data|
puts black_on_yellow { 'on_data'.ljust(12) }, data
data
end
end
def on_response
lambda do |backend, resp|
puts black_on_green { 'on_response'.ljust(12) } + " from #{backend}", resp
resp
end
end
def on_finish
lambda do |backend|
puts black_on_cyan { 'on_finish'.ljust(12) } + " for #{backend}", ''
backend.decrement_counter if Backend.strategy == :balanced
end
end
end
# Wrapping the proxy server
#
module Server
def run(host='0.0.0.0', port=9999)
puts ANSI::Code.bold { "Launching proxy at #{host}:#{port}...\n" }
Proxy.start(:host => host, :port => port, :debug => false) do |conn|
Backend.select do |backend|
conn.server backend, :host => backend.host, :port => backend.port
conn.on_connect &Callbacks.on_connect
conn.on_data &Callbacks.on_data
conn.on_response &Callbacks.on_response
conn.on_finish &Callbacks.on_finish
end
end
end
module_function :run
end
end
if __FILE__ == $0
require 'rack'
class Proxy
def self.stop
puts "Terminating ProxyServer"
EventMachine.stop
$servers.each do |pid|
puts "Terminating webserver #{pid}"
Process.kill('KILL', pid)
end
end
end
# Simple Rack app to run
app = proc { |env| [ 200, {'Content-Type' => 'text/plain'}, ["Hello World!"] ] }
# Run app on ports 3000-3002
$servers = []
3.times do |i|
$servers << Process.fork { Rack::Handler::WEBrick.run(app, {:Host => "0.0.0.0", :Port => "300#{i}"}) }
end
puts ANSI::Code::green_on_black { "\n=> Send multiple requests to the proxy by running `ruby balancing-client.rb`\n" }
# Start proxy
BalancingProxy::Server.run
end