Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit d66dc25a853b42d3abf9379339fb89faec1af05d 0 parents
Cloud Foundry Engineer authored
16 Gemfile
@@ -0,0 +1,16 @@
+source "http://rubygems.org"
+
+gem 'bundler', '>= 1.0.10'
+gem 'rake'
+gem 'nats', '>= 0.4.8', :require => 'nats/client'
+gem 'eventmachine', '~> 0.12.10'
+gem "http_parser.rb", :require => "http/parser"
+gem "yajl-ruby", :require => ["yajl", "yajl/json_gem"]
+
+gem 'vcap_common', :path => '../common'
+
+group :test do
+ gem "rspec"
+ gem "rcov"
+ gem "ci_reporter"
+end
55 Gemfile.lock
@@ -0,0 +1,55 @@
+PATH
+ remote: ../common
+ specs:
+ vcap_common (0.99)
+ eventmachine (~> 0.12.10)
+ nats
+ thin
+ yajl-ruby
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ builder (3.0.0)
+ ci_reporter (1.6.4)
+ builder (>= 2.1.2)
+ daemons (1.1.2)
+ diff-lcs (1.1.2)
+ eventmachine (0.12.10)
+ http_parser.rb (0.5.1)
+ json_pure (1.5.1)
+ nats (0.4.8)
+ daemons (>= 1.1.0)
+ eventmachine (>= 0.12.10)
+ json_pure (>= 1.5.1)
+ rack (1.2.2)
+ rake (0.8.7)
+ rcov (0.9.9)
+ rspec (2.5.0)
+ rspec-core (~> 2.5.0)
+ rspec-expectations (~> 2.5.0)
+ rspec-mocks (~> 2.5.0)
+ rspec-core (2.5.1)
+ rspec-expectations (2.5.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.5.0)
+ thin (1.2.11)
+ daemons (>= 1.0.9)
+ eventmachine (>= 0.12.6)
+ rack (>= 1.0.0)
+ yajl-ruby (0.8.2)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (>= 1.0.10)
+ ci_reporter
+ eventmachine (~> 0.12.10)
+ http_parser.rb
+ nats (>= 0.4.8)
+ rake
+ rcov
+ rspec
+ vcap_common!
+ yajl-ruby
55 Rakefile
@@ -0,0 +1,55 @@
+require 'rake'
+
+desc "Run specs"
+task "spec" => ["bundler:install:test", "test:spec"]
+
+desc "Run specs in CI mode"
+# FIXME - Router specs currently fail on some platforms if they
+# share a bundle directory with the rest of the core.
+# Some kind of tricky interaction around the --without flag?
+task "ci" do
+ sh("BUNDLE_PATH=$HOME/.vcap_router_gems bundle install --without production")
+ Dir.chdir("spec") do
+ sh("BUNDLE_PATH=$HOME/.vcap_router_gems bundle exec rake spec")
+ end
+end
+
+desc "Run specs using RCov"
+task "spec:rcov" => ["bundler:install:test", "test:spec:rcov"]
+
+desc "Synonym for spec"
+task :test => :spec
+desc "Synonym for spec"
+task :tests => :spec
+
+namespace "bundler" do
+ desc "Install gems"
+ task "install" do
+ sh("bundle install")
+ end
+
+ desc "Install gems for test"
+ task "install:test" do
+ sh("bundle install --without development production")
+ end
+
+ desc "Install gems for production"
+ task "install:production" do
+ sh("bundle install --without development test")
+ end
+
+ desc "Install gems for development"
+ task "install:development" do
+ sh("bundle install --without test production")
+ end
+end
+
+namespace "test" do
+ task "spec" do |t|
+ sh("cd spec && rake spec")
+ end
+
+ task "spec:rcov" do |t|
+ sh("cd spec && rake spec:rcov")
+ end
+end
6 bin/router
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# Copyright (c) 2009-2011 VMware, Inc.
+
+home = File.join(File.dirname(__FILE__), '/..')
+ENV['BUNDLE_GEMFILE'] = "#{home}/Gemfile"
+require File.join(home, 'lib/router')
6 config/router.yml
@@ -0,0 +1,6 @@
+port: 2222
+inet: 0.0.0.0
+sock: /tmp/router.sock
+mbus: nats://localhost:4222
+log_level: INFO
+pid: /var/vcap/sys/run/router.pid
6 config/router2.yml
@@ -0,0 +1,6 @@
+port: 2224
+inet: 0.0.0.0
+sock: /tmp/router2.sock
+mbus: nats://localhost:4222
+log_level: INFO
+pid: /var/vcap/sys/run/router2.pid
149 lib/router.rb
@@ -0,0 +1,149 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+require 'fileutils'
+require 'logger'
+require 'optparse'
+require 'socket'
+require 'yaml'
+require 'openssl'
+
+require 'rubygems'
+require 'bundler/setup'
+
+require 'nats/client'
+require 'http/parser'
+
+require 'vcap/common'
+require 'vcap/component'
+
+$:.unshift(File.dirname(__FILE__))
+
+require 'router/const'
+require 'router/router'
+require 'router/app_connection'
+require 'router/client_connection'
+require 'router/utils'
+
+config_file = File.join(File.dirname(__FILE__), '../config/router.yml')
+port, inet = nil, nil
+
+options = OptionParser.new do |opts|
+ opts.banner = 'Usage: router [OPTIONS]'
+ opts.on("-p", "--port [ARG]", "Network port") do |opt|
+ port = opt.to_i
+ end
+ opts.on("-i", "--interface [ARG]", "Network Interface") do |opt|
+ inet = opt
+ end
+ opts.on("-c", "--config [ARG]", "Configuration File") do |opt|
+ config_file = opt
+ end
+ opts.on("-h", "--help", "Help") do
+ puts opts
+ exit
+ end
+end
+options.parse!(ARGV.dup)
+
+begin
+ config = File.open(config_file) do |f|
+ YAML.load(f)
+ end
+rescue => e
+ puts "Could not read configuration file: #{e}"
+ exit
+end
+
+# Placeholder for Component reporting
+config['config_file'] = File.expand_path(config_file)
+
+port = config['port'] unless port
+inet = config['inet'] unless inet
+
+EM.epoll
+
+EM.run {
+
+ trap("TERM") { stop(config['pid']) }
+ trap("INT") { stop(config['pid']) }
+
+ Router.config(config)
+ Router.log.info "Starting VCAP Router (#{Router.version})"
+ Router.log.info "Listening on: #{inet}:#{port}" if inet && port
+
+ Router.inet = inet || VCAP.local_ip(config['local_route'])
+ Router.port = port
+
+ # If the sock paramater is set, this will override the inet/port
+ # for unix domain sockets
+ if fn = config['sock']
+ File.unlink(fn) if File.exists?(fn)
+ Router.log.info "Listening on unix domain socket: '#{fn}'"
+ end
+
+ # Hack for running on BVTs on Macs which default to 256 FDs per process
+ if RUBY_PLATFORM =~ /darwin/
+ begin
+ Process.setrlimit(Process::RLIMIT_NOFILE, 4096)
+ rescue => e
+ Router.log.info "Failed to modify the socket limit: #{e}"
+ end
+ end
+
+ EM.set_descriptor_table_size(32768) # Requires Root privileges
+ Router.log.info "Socket Limit:#{EM.set_descriptor_table_size}"
+
+ create_pid_file(config['pid'])
+
+ EM.error_handler { |e|
+ if e.kind_of? NATS::Error
+ Router.log.error("NATS problem, #{e}")
+ else
+ Router.log.error "Eventmachine problem, #{e}"
+ Router.log.error("#{e.backtrace.join("\n")}")
+ end
+ }
+
+ begin
+ # TCP/IP Socket
+ Router.server = EM.start_server(inet, port, ClientConnection, false) if inet && port
+ Router.local_server = EM.start_server(fn, nil, ClientConnection, true) if fn
+ rescue => e
+ Router.log.fatal "Problem starting server, #{e}"
+ exit
+ end
+
+ # Allow nginx to access..
+ FileUtils.chmod(0777, fn) if fn
+
+ NATS.start(:uri => config['mbus'])
+
+ # Create the register/unregister listeners.
+ Router.setup_listeners
+
+ # Register ourselves with the system
+ VCAP::Component.register(:type => 'Router',
+ :host => VCAP.local_ip(config['local_route']),
+ :config => config)
+
+ # Setup some of our varzs..
+ VCAP::Component.varz[:requests] = 0
+ VCAP::Component.varz[:bad_requests] = 0
+ VCAP::Component.varz[:urls] = 0
+ VCAP::Component.varz[:droplets] = 0
+
+ @router_id = VCAP.fast_uuid
+ @hello_message = { :id => @router_id, :version => Router::VERSION }.to_json.freeze
+
+ Router.log_connection_stats
+
+ # This will check on the state of the registered urls, do maintenance, etc..
+ Router.setup_sweepers
+
+ # Setup a start sweeper to make sure we have a consistent view of the world.
+ EM.next_tick {
+ # Announce our existence
+ NATS.publish('router.start', @hello_message)
+ EM.add_periodic_timer(START_SWEEPER) { NATS.publish('router.start', @hello_message) }
+ }
+}
+
162 lib/router/app_connection.rb
@@ -0,0 +1,162 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+module AppConnection
+
+ attr_reader :oustanding_requests
+
+ def initialize(client, request, droplet)
+ Router.log.debug "Creating AppConnection"
+ @client, @request, @droplet = client, request, droplet
+ @connected = false
+ @outstanding_requests = 1
+ Router.outstanding_request_count += 1
+ end
+
+ def post_init
+ VCAP::Component.varz[:app_connections] = Router.app_connection_count += 1
+ Router.log.debug "Completed AppConnection"
+ Router.log.debug Router.connection_stats
+ Router.log.debug "------------"
+ end
+
+ def connection_completed
+ @connected = true
+ #proxy_incoming_to(@client) if @client
+ send_data(@request) if @client && @request
+ end
+
+ # queue data until connection completed.
+ def deliver_data(data)
+ return send_data(data) if @connected
+ @request << data
+ end
+
+ # We have the HTTP Headers complete from the client
+ def on_headers_complete(headers)
+ check_sticky_session = STICKY_SESSIONS =~ headers[SET_COOKIE_HEADER]
+ sent_session_cookie = false # Only send one in case of multiple hits
+
+ header_lines = @headers.split("\r\n")
+ header_lines.each do |line|
+ @client.send_data(line)
+ @client.send_data(CR_LF)
+ if (check_sticky_session && !sent_session_cookie && STICKY_SESSIONS =~ line)
+ sid = Router.generate_session_cookie(@droplet)
+ scl = line.sub(/\S+\s*=\s*\w+/, "#{VCAP_SESSION_ID}=#{sid}")
+ sent_session_cookie = true
+ @client.send_data(scl)
+ @client.send_data(CR_LF)
+ end
+ end
+ # Trace if properly requested
+ if @client.trace
+ router_trace = "#{VCAP_ROUTER_HEADER}:#{Router.inet}#{CR_LF}"
+ be_trace = "#{VCAP_BACKEND_HEADER}:#{@droplet[:host]}:#{@droplet[:port]}#{CR_LF}"
+ @client.send_data(router_trace)
+ @client.send_data(be_trace)
+ end
+ # Ending CR_LF
+ @client.send_data(CR_LF)
+ end
+
+ def process_response_body_chunk(data)
+ return unless data
+
+ # Let parser process as well to properly determine end of message.
+ # TODO: Once EM 1.0, add in optional bytsize proxy if Content-Length is present.
+ psize = @parser << data
+ if (psize == data.bytesize)
+ @client.send_data(data)
+ else
+ Router.log.info "Pipelined response detected!"
+ # We have a pipelined response, we need to hansd over the new headers and only send the proper
+ # body segments to the backend
+ body = data.slice!(0, psize)
+ @client.send_data(body)
+ receive_data(data)
+ end
+ end
+
+ def on_message_complete
+ @parser = nil
+ :stop
+ @outstanding_requests -= 1
+ Router.outstanding_request_count -= 1
+ end
+
+ def cant_be_recycled?
+ error? || @parser != nil || @connected == false || @outstanding_requests > 0
+ end
+
+ def recycle
+ stop_proxying
+ @client = @request = @headers = nil
+ end
+
+ def receive_data(data)
+ # Parser is created after headers have been received and processed.
+ # If it exists we are continuing the processing of body fragments.
+ # Allow the parser to process to signal proper end of message, e.g. chunked, etc
+ return process_response_body_chunk(data) if @parser
+
+ # We are awaiting headers here.
+ # We buffer them if needed to determine the header/body boundary correctly.
+ @buf = @buf ? @buf << data : data
+ if hindex = @buf.index(HTTP_HEADERS_END) # all http headers received, figure out where to route to..
+ @parser = Http::Parser.new(self)
+
+ # split headers and rest of body out here.
+ @headers = @buf.slice!(0...hindex+HTTP_HEADERS_END_SIZE)
+
+ # Process headers
+ @parser << @headers
+
+ # Process left over body fragment if any
+ process_response_body_chunk(@buf) if @parser
+ @buf = @headers = nil
+ end
+
+ rescue Http::Parser::Error => e
+ Router.log.debug "HTTP Parser error on response: #{e}"
+ close_connection
+ end
+
+ def rebind(client, request)
+ @client = client
+ reuse(request)
+ end
+
+ def reuse(new_request)
+ @request = new_request
+ @outstanding_requests += 1
+ Router.outstanding_request_count += 1
+ deliver_data(@request)
+ end
+
+ def proxy_target_unbound
+ Router.log.debug "Proxy connection dropped"
+ #close_connection_after_writing
+ end
+
+ def unbind
+ Router.outstanding_request_count -= @outstanding_requests
+ unless @connected
+ Router.log.info "Could not connect to backend for url:#{@droplet[:url]} @ #{@droplet[:host]}:#{@droplet[:port]}"
+ if @client
+ @client.send_data(Router.notfound_redirect || ERROR_404_RESPONSE)
+ @client.close_connection_after_writing
+ end
+ # TODO(dlc) fix - We will unregister bad backends here, should retry the request if possible.
+ Router.unregister_droplet(@droplet[:url], @droplet[:host], @droplet[:port])
+ end
+
+ VCAP::Component.varz[:app_connections] = Router.app_connection_count -= 1
+ Router.log.debug 'Unbinding AppConnection'
+ Router.log.debug Router.connection_stats
+ Router.log.debug "------------"
+
+ # Remove ourselves from the connection pool
+ @droplet[:connections].delete(self)
+
+ @client.close_connection_after_writing if @client
+ end
+end
206 lib/router/client_connection.rb
@@ -0,0 +1,206 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+module ClientConnection
+
+ HTTP_11 ='11'.freeze
+
+ attr_reader :close_connection_after_request, :trace
+
+ def initialize(is_unix_socket)
+ Router.log.debug "Created Client Connection"
+ @droplet, @bound_app_conn = nil, nil, nil
+ # Default to be on the safe side
+ @close_connection_after_request = true
+ @is_unix_socket = is_unix_socket
+ end
+
+ def post_init
+ VCAP::Component.varz[:client_connections] = Router.client_connection_count += 1
+ Router.log.debug Router.connection_stats
+ Router.log.debug "------------"
+ self.comm_inactivity_timeout = 60.0
+ end
+
+ def recycle_app_conn
+ return if (!@droplet || !@bound_app_conn) # Could we leak @bound_app_conn here?
+
+ # Don't recycle if we are not supposed to..
+ if @close_connection_after_request
+ Router.log.debug "NOT placing bound AppConnection back into free list because a close was requested.."
+ @bound_app_conn.close_connection_after_writing
+ return
+ end
+
+ # Place any bound connections back into the droplets connection pool.
+ # This happens on client connection reuse, HTTP 1.1.
+ # Check for errors, overcommits, etc..
+
+ if (@bound_app_conn.cant_be_recycled?)
+ Router.log.debug "NOT placing AppConnection back into free list, can't be recycled"
+ @bound_app_conn.close_connection_after_writing
+ return
+ end
+
+ if @droplet[:connections].index(@bound_app_conn)
+ Router.log.debug "NOT placing AppConnection back into free list, already exists in free list.."
+ return
+ end
+
+ if @droplet[:connections].size >= MAX_POOL
+ Router.log.debug "NOT placing AppConnection back into free list, MAX_POOL connections already exist.."
+ @bound_app_conn.close_connection_after_writing
+ return
+ end
+
+ Router.log.debug "Placing bound AppConnection back into free list.."
+ @bound_app_conn.recycle
+ @droplet[:connections].push(@bound_app_conn)
+ @bound_app_conn = nil
+ end
+
+ def terminate_app_conn
+ return unless @bound_app_conn
+ Router.log.debug "Terminating AppConnection"
+ @bound_app_conn.stop_proxying
+ @bound_app_conn.close_connection
+ @bound_app_conn = nil
+ end
+
+ def process_request_body_chunk(data)
+ return unless data
+ if (@droplet && @bound_app_conn && !@bound_app_conn.error?)
+
+ # Let parser process as well to properly determine end of message.
+ psize = @parser << data
+
+ if (psize != data.bytesize)
+ Router.log.info "Pipelined request detected!"
+ # We have a pipelined request, we need to hand over the new headers and only send the proper
+ # body segments to the backend
+ body = data.slice!(0, psize)
+ @bound_app_conn.deliver_data(body)
+ receive_data(data)
+ else
+ @bound_app_conn.deliver_data(data)
+ end
+ else # We do not have a backend droplet anymore
+ Router.log.info "Backend connection dropped"
+ terminate_app_conn
+ close_connection # Should we retry here?
+ end
+ end
+
+ # We have the HTTP Headers complete from the client
+ def on_headers_complete(headers)
+ return close_connection unless headers and host = headers[HOST_HEADER]
+
+ # Support for HTTP/1.1 connection reuse and possible pipelining
+ @close_connection_after_request = (@parser.http_version.to_s == HTTP_11) ? false : true
+
+ # Support for Connection:Keep-Alive requests on HTTP/1.0, e.g. ApacheBench
+ @close_connection_after_request = false if (headers[CONNECTION_HEADER] == KEEP_ALIVE)
+
+ @trace = (headers[VCAP_TRACE_HEADER] == Router.trace_key)
+
+ # Update # of requests..
+ VCAP::Component.varz[:requests] += 1
+
+ # Clear and recycle previous state
+ recycle_app_conn if @bound_app_conn
+ @droplet = @bound_app_conn = nil
+
+ # Lookup a Droplet
+ unless droplets = Router.lookup_droplet(host)
+ Router.log.debug "No droplet registered for #{host}"
+ VCAP::Component.varz[:bad_requests] += 1
+ send_data(Router.notfound_redirect || ERROR_404_RESPONSE)
+ close_connection_after_writing
+ return
+ end
+
+ Router.log.debug "#{droplets.size} servers available for #{host}"
+
+ # Check for session state
+ if VCAP_COOKIE =~ headers[COOKIE_HEADER]
+ url, host, port = Router.decrypt_session_cookie($1)
+ Router.log.debug "Client has __VCAP_ID__ for #{url}@#{host}:#{port}"
+ # Check host?
+ droplets.each { |droplet|
+ # If we already now about them just update the timestamp..
+ if(droplet[:host] == host && droplet[:port] == port)
+ @droplet = droplet
+ break;
+ end
+ }
+ Router.log.debug "Client's __VCAP_ID__ is stale" unless @droplet
+ end
+
+ # pick a random backend unless selected from above already
+ @droplet = droplets[rand*droplets.size] unless @droplet
+
+ @droplet[:requests] += 1
+
+ # Client tracking, override with header if its set (nginx to unix domain socket)
+ _, client_ip = Socket.unpack_sockaddr_in(get_peername) unless @is_unix_socket
+ client_ip = headers[REAL_IP_HEADER] if headers[REAL_IP_HEADER]
+
+ @droplet[:clients][client_ip] += 1 if client_ip
+
+ Router.log.debug "Routing on #{@droplet[:url]} to #{@droplet[:host]}:#{@droplet[:port]}"
+
+ # Reuse an existing connection or create one.
+ # Proxy the rest of the traffic without interference.
+ Router.log.debug "Droplet has #{@droplet[:connections].size} pooled connections waiting.."
+ @bound_app_conn = @droplet[:connections].pop
+ if (@bound_app_conn && !@bound_app_conn.error?)
+ Router.log.debug "Reusing pooled AppConnection.."
+ @bound_app_conn.rebind(self, @headers)
+ else
+ host, port = @droplet[:host], @droplet[:port]
+ @bound_app_conn = EM.connect(host, port, AppConnection, self, @headers, @droplet)
+ end
+
+ end
+
+ def on_message_complete
+ @parser = nil
+ :stop
+ end
+
+ def receive_data(data)
+ # Parser is created after headers have been received and processed.
+ # If it exists we are continuing the processing of body fragments.
+ # Allow the parser to process to signal proper end of message, e.g. chunked, etc
+ return process_request_body_chunk(data) if @parser
+
+ # We are awaiting headers here.
+ # We buffer them if needed to determine the header/body boundary correctly.
+ @buf = @buf ? @buf << data : data
+
+ if hindex = @buf.index(HTTP_HEADERS_END) # all http headers received, figure out where to route to..
+ @parser = Http::Parser.new(self)
+
+ # split headers and rest of body out here.
+ @headers = @buf.slice!(0...hindex+HTTP_HEADERS_END_SIZE)
+
+ # Process headers
+ @parser << @headers
+
+ # Process left over body fragment if any
+ process_request_body_chunk(@buf) if @parser
+ @buf = @headers = nil
+ end
+
+ rescue Http::Parser::Error => e
+ Router.log.debug "HTTP Parser error on request: #{e}"
+ close_connection
+ end
+
+ def unbind
+ Router.log.debug "Unbinding client connection"
+ VCAP::Component.varz[:client_connections] = Router.client_connection_count -= 1
+ Router.log.debug Router.connection_stats
+ Router.log.debug "------------"
+ @close_connection_after_request ? terminate_app_conn : recycle_app_conn
+ end
+
+end
35 lib/router/const.rb
@@ -0,0 +1,35 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+# HTTP Header processing
+HOST_HEADER = 'Host'.freeze
+CONNECTION_HEADER = 'Connection'.freeze
+REAL_IP_HEADER = 'X-Real_IP'.freeze
+HTTP_HEADERS_END = "\r\n\r\n".freeze
+HTTP_HEADERS_END_SIZE = HTTP_HEADERS_END.bytesize
+KEEP_ALIVE = 'keep-alive'.freeze
+SET_COOKIE_HEADER = 'Set-Cookie'.freeze
+COOKIE_HEADER = 'Cookie'.freeze
+CR_LF = "\r\n".freeze
+
+STICKY_SESSIONS = /(JSESSIONID)/i
+
+VCAP_SESSION_ID = '__VCAP_ID__'.freeze
+VCAP_COOKIE = /__VCAP_ID__=([^;]+)/
+
+VCAP_BACKEND_HEADER = 'X-Vcap-Backend'
+VCAP_ROUTER_HEADER = 'X-Vcap-Router'
+VCAP_TRACE_HEADER = 'X-Vcap-Trace'
+
+# Max Connections to Pool
+MAX_POOL = 32
+
+# Timers for sweepers
+
+RPS_SWEEPER = 2 # Requests rate sweeper
+START_SWEEPER = 30 # Timer to publish router.start for refreshing state
+CHECK_SWEEPER = 30 # Check time for watching health of registered drop
+MAX_AGE_STALE = 120 # Max stale age, unregistered if older then 2 minutes
+
+# 404 Response
+ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n" +
+ "VCAP ROUTER: 404 - DESTINATION NOT FOUND\r\n".freeze
+
192 lib/router/router.rb
@@ -0,0 +1,192 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+class Router
+
+ VERSION = 0.98
+
+ class << self
+ attr_reader :log, :notfound_redirect, :session_key, :trace_key
+ attr_accessor :server, :local_server, :timestamp, :shutting_down
+ attr_accessor :client_connection_count, :app_connection_count, :outstanding_request_count
+ attr_accessor :inet, :port
+
+ alias :shutting_down? :shutting_down
+
+ def version
+ VERSION
+ end
+
+ def config(config)
+ @droplets = {}
+ @client_connection_count = @app_connection_count = @outstanding_request_count = 0
+ @log = Logger.new(config['log_file'] ? config['log_file'] : STDOUT, 'daily')
+ @log.level = case config['log_level']
+ when 'DEBUG' then Logger::DEBUG
+ when 'INFO' then Logger::INFO
+ when 'WARN' then Logger::WARN
+ when 'ERROR' then Logger::ERROR
+ when 'FATAL' then Logger::FATAL
+ else Logger::UNKNOWN
+ end
+
+ if config['404_redirect']
+ @notfound_redirect = "HTTP/1.1 302 Not Found\r\nConnection: close\r\nLocation: #{config['404_redirect']}\r\n\r\n".freeze
+ log.info "Registered 404 redirect at #{config['404_redirect']}"
+ end
+
+ @session_key = config['session_key'] || '14fbc303b76bacd1e0a3ab641c11d11400341c5d'
+ @trace_key = config['trace_key'] || '22'
+ end
+
+ def setup_listeners
+ NATS.subscribe('router.register') { |msg|
+ msgHash = Yajl::Parser.parse(msg, :symbolize_keys => true)
+ return unless uris = msgHash[:uris]
+ uris.each { |uri| register_droplet(uri, msgHash[:host], msgHash[:port]) }
+ }
+ NATS.subscribe('router.unregister') { |msg|
+ msgHash = Yajl::Parser.parse(msg, :symbolize_keys => true)
+ return unless uris = msgHash[:uris]
+ uris.each { |uri| unregister_droplet(uri, msgHash[:host], msgHash[:port]) }
+ }
+ end
+
+ def setup_sweepers
+ @rps_timestamp = Time.now
+ @current_num_requests = 0
+ EM.add_periodic_timer(RPS_SWEEPER) { calc_rps }
+ EM.add_periodic_timer(CHECK_SWEEPER) {
+ check_registered_urls
+ log_connection_stats
+ }
+ end
+
+ def calc_rps
+ # Update our timestamp and calculate delta for reqs/sec
+ now = Time.now
+ delta = (now - @rps_timestamp).to_f
+ @rps_timestamp = now
+
+ # Now calculate Requests/sec
+ new_num_requests = VCAP::Component.varz[:requests]
+ VCAP::Component.varz[:requests_per_sec] = ((new_num_requests - @current_num_requests)/delta).to_i
+ @current_num_requests = new_num_requests
+
+ # Go ahead and calculate rates for all backends here.
+ apps = []
+ @droplets.each_pair do |url, instances|
+ total_requests = 0
+ clients_hash = Hash.new(0)
+ instances.each do |droplet|
+ total_requests += droplet[:requests]
+ droplet[:requests] = 0
+ droplet[:clients].each_pair { |ip,req| clients_hash[ip] += req }
+ droplet[:clients] = Hash.new(0) # Wipe these per sweep
+ end
+
+ # Grab top 5 clients responsible for the traffic
+ clients, clients_array = [], clients_hash.sort { |a,b| b[1]<=>a[1] } [0,4]
+ clients_array.each { |c| clients << { :ip => c[0], :rps => (c[1]/delta).to_i } }
+
+ # Add in clients if they exist and the entry if rps != 0
+ if (rps = (total_requests/delta).to_i) > 0
+ entry = { :url => url, :rps => rps }
+ entry[:clients] = clients unless clients.empty?
+ apps << entry unless entry[:rps] == 0
+ end
+ end
+
+ top_10 = apps.sort { |a,b| b[:rps]<=>a[:rps] } [0,9]
+ VCAP::Component.varz[:top_app_requests] = top_10
+ #log.debug "Calculated all request rates in #{Time.now - now} secs."
+ end
+
+ def check_registered_urls
+ start = Time.now
+ to_drop = []
+ @droplets.each_pair do |url, instances|
+ instances.each do |droplet|
+ to_drop << droplet if ((start - droplet[:timestamp]) > MAX_AGE_STALE)
+ end
+ end
+ log.debug "Checked all registered URLS in #{Time.now - start} secs."
+ to_drop.each { |droplet| unregister_droplet(droplet[:url], droplet[:host], droplet[:port]) }
+ end
+
+ def connection_stats
+ tc = EM.connection_count
+ ac = Router.app_connection_count
+ cc = Router.client_connection_count
+ "Connections: [Clients: #{cc}, Apps: #{ac}, Total: #{tc}]"
+ end
+
+ def log_connection_stats
+ tc = EM.connection_count
+ ac = Router.app_connection_count
+ cc = Router.client_connection_count
+ log.info connection_stats
+ end
+
+ def generate_session_cookie(droplet)
+ token = [ droplet[:url], droplet[:host], droplet[:port] ]
+ c = OpenSSL::Cipher::Cipher.new('blowfish')
+ c.encrypt
+ c.key = @session_key
+ e = c.update(Marshal.dump(token))
+ e << c.final
+ [e].pack('m0').gsub("\n",'')
+ end
+
+ def decrypt_session_cookie(key)
+ e = key.unpack('m*')[0]
+ d = OpenSSL::Cipher::Cipher.new('blowfish')
+ d.decrypt
+ d.key = @session_key
+ p = d.update(e)
+ p << d.final
+ Marshal.load(p)
+ rescue
+ nil
+ end
+
+ def lookup_droplet(url)
+ @droplets[url]
+ end
+
+ def register_droplet(url, host, port)
+ return unless host && port
+ url.downcase!
+ droplets = @droplets[url] || []
+ # Skip the ones we already know about..
+ droplets.each { |droplet|
+ # If we already now about them just update the timestamp..
+ if(droplet[:host] == host && droplet[:port] == port)
+ droplet[:timestamp] = Time.now
+ return
+ end
+ }
+ droplet = {
+ :host => host, :port => port, :connections => [], :clients => Hash.new(0),
+ :url => url, :timestamp => Time.now, :requests => 0
+ }
+ droplets << droplet
+ @droplets[url] = droplets
+ VCAP::Component.varz[:urls] = @droplets.size
+ VCAP::Component.varz[:droplets] += 1
+ log.info "Registering #{url} at #{host}:#{port}"
+ log.info "#{droplets.size} servers available for #{url}"
+ end
+
+ def unregister_droplet(url, host, port)
+ log.info "Unregistering #{url} for host #{host}:#{port}"
+ url.downcase!
+ droplets = @droplets[url] || []
+ dsize = droplets.size
+ droplets.delete_if { |d| d[:host] == host && d[:port] == port}
+ @droplets.delete(url) if droplets.empty?
+ VCAP::Component.varz[:urls] = @droplets.size
+ VCAP::Component.varz[:droplets] -= 1 unless (dsize == droplets.size)
+ log.info "#{droplets.size} servers available for #{url}"
+ end
+
+ end
+end
35 lib/router/utils.rb
@@ -0,0 +1,35 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+def create_pid_file(pidfile)
+ # Make sure dirs exist.
+ begin
+ FileUtils.mkdir_p(File.dirname(pidfile))
+ rescue => e
+ Router.log.fatal "Can't create pid directory, exiting: #{e}"
+ end
+ File.open(pidfile, 'w') { |f| f.puts "#{Process.pid}" }
+end
+
+def stop(pidfile)
+ # Double ctrl-c just terminates
+ exit if Router.shutting_down?
+ Router.shutting_down = true
+ Router.log.info 'Signal caught, shutting down..'
+ Router.log.info 'waiting for pending requests to complete.'
+ EM.stop_server(Router.server) if Router.server
+ EM.stop_server(Router.local_server) if Router.local_server
+ if Router.outstanding_request_count <= 0
+ exit_router(pidfile)
+ else
+ EM.add_periodic_timer(0.25) { exit_router(pidfile) if (Router.outstanding_request_count <= 0) }
+ EM.add_timer(10) { exit_router(pidfile) } # Wait at most 10 secs
+ end
+
+end
+
+def exit_router(pidfile)
+ NATS.stop { EM.stop }
+ Router.log.info "Bye"
+ FileUtils.rm_f(pidfile)
+ exit
+end
+
42 spec/Rakefile
@@ -0,0 +1,42 @@
+require 'rake'
+require 'tempfile'
+
+require 'rubygems'
+require 'bundler/setup'
+Bundler.require(:default, :test)
+
+require 'rspec/core/rake_task'
+require 'ci/reporter/rake/rspec'
+
+coverage_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_coverage"))
+reports_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_reports"))
+dump_file = File.join(Dir.tmpdir, "router.rcov")
+
+ENV['CI_REPORTS'] = reports_dir
+
+desc "Run specs using RCov"
+task "spec:rcov" => ["ci:setup:rspec", "spec:rcov_internal", "convert_rcov_to_clover"]
+
+RSpec::Core::RakeTask.new do |t|
+ t.pattern = "**/*_spec.rb"
+ t.rspec_opts = ["--format", "documentation", "--colour"]
+end
+
+ignore_pattern = 'spec,[.]bundle,[/]gems[/]'
+
+desc "Run specs using RCov (internal, use spec:rcov instead)"
+RSpec::Core::RakeTask.new("spec:rcov_internal") do |t|
+ FileUtils.rm_rf(dump_file)
+ t.pattern = "**/*_spec.rb"
+ t.rspec_opts = ["--format", "progress", "--colour"]
+ t.rcov = true
+ t.rcov_opts = ['--aggregate', dump_file, '--exclude', ignore_pattern, '--output', coverage_dir]
+# t.rcov_opts = %W{--exclude osx\/objc,gems\/,spec\/,features\/ -o "#{coverage_dir}"}
+end
+
+task "convert_rcov_to_clover" do |t|
+ clover_output = File.join(coverage_dir, "clover.xml")
+ analyzer = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "tests", "common", "rcov_analyzer.rb"))
+ sh("ruby #{analyzer} #{dump_file} #{ignore_pattern} > #{clover_output}")
+ FileUtils.rm_rf(dump_file)
+end
244 spec/functional/router_spec.rb
@@ -0,0 +1,244 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+require File.dirname(__FILE__) + '/spec_helper'
+
+describe 'Router Functional Tests' do
+
+ before :all do
+ @nats_server = NatsServer.new
+ @router = RouterServer.new(@nats_server.uri)
+ end
+
+ after :all do
+ @router.kill_server
+ @router.is_running?.should be_false
+
+ @nats_server.kill_server
+ @nats_server.is_running?.should be_false
+ end
+
+ it 'should start nats correctly' do
+ @nats_server.start_server
+ @nats_server.is_running?.should be_true
+ end
+
+ it 'should start router correctly' do
+ # This avoids a race in the next test ('should respond to a discover message properly')
+ # between when we issue a request and when the router has subscribed. (The router will
+ # only announce itself after it has subscribed to 'vcap.component.discover'.)
+ NATS.start(:uri => @nats_server.uri) do
+ NATS.subscribe('vcap.component.announce') { NATS.stop }
+ # Ensure that NATS has processed our subscribe from above before we start the router
+ NATS.publish('xxx') { @router.start_server }
+ EM.add_timer(5) { NATS.stop }
+ end
+ @router.is_running?.should be_true
+ end
+
+ it 'should respond to a discover message properly' do
+ reply = nil
+ NATS.start(:uri => @nats_server.uri) do
+ NATS.request('vcap.component.discover') do |msg|
+ reply = JSON.parse(msg, :symbolize_keys => true)
+ NATS.stop
+ end
+ EM.add_timer(1) { NATS.stop }
+ end
+ reply[:type].should =~ /router/i
+ reply.should have_key :uuid
+ reply.should have_key :host
+ reply.should have_key :start
+ reply.should have_key :uptime
+ ROUTER_HOST = reply[:host]
+ end
+
+ it 'should have proper http endpoints (/healthz, /varz)' do
+ credentials = nil
+ NATS.start(:uri => @nats_server.uri) do
+ NATS.request('vcap.component.discover') do |msg|
+ reply = JSON.parse(msg, :symbolize_keys => true)
+ credentials = reply[:credentials]
+ NATS.stop
+ end
+ EM.add_timer(1) { NATS.stop }
+ end
+
+ host, port = ROUTER_HOST.split(":")
+
+ healthz_req = Net::HTTP::Get.new("/healthz")
+ healthz_req.basic_auth *credentials
+ healthz_resp = Net::HTTP.new(host, port).start { |http| http.request(healthz_req) }
+ healthz_resp.body.should =~ /ok/i
+
+ varz_req = Net::HTTP::Get.new("/varz")
+ varz_req.basic_auth *credentials
+ varz_resp = Net::HTTP.new(host, port).start { |http| http.request(varz_req) }
+ varz = JSON.parse(varz_resp.body, :symbolize_keys => true)
+ varz[:requests].should be_a_kind_of(Integer)
+ varz[:bad_requests].should be_a_kind_of(Integer)
+ varz[:client_connections].should be_a_kind_of(Integer)
+ varz[:type].should =~ /router/i
+ end
+
+ it 'should properly register an application endpoint' do
+ # setup the "app"
+ app_socket, app_port = new_app_socket
+
+ r = {:dea => '1234', :host => '127.0.0.1', :port => app_port, :uris => ['router_test.vcap.me']}
+
+ NATS.start(:uri => @nats_server.uri) do
+ # Registration Message
+ NATS.publish('router.register', r.to_json) { NATS.stop }
+ end
+
+ req = simple_http_request('router_test.vcap.me', '/')
+
+ # We should be registered here..
+ # Send out simple request and check request and response
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(req, 0)
+ IO.select([app_socket], nil, nil, 2) # 2 secs timeout
+ ss = app_socket.accept_nonblock
+ req_received = ss.recv(req.bytesize)
+ req_received.should == req
+ # Send a response back..
+ ss.send(FOO_HTTP_RESPONSE, 0)
+ response = rs.read(FOO_HTTP_RESPONSE.bytesize)
+ response.should == FOO_HTTP_RESPONSE
+ ss.close
+ end
+ app_socket.close
+ APP_PORT = app_port
+ end
+
+ it 'should properly unregister an application endpoint' do
+ r = {:dea => '1234', :host => '127.0.0.1', :port => APP_PORT, :uris => ['router_test.vcap.me']}
+ req = simple_http_request('router_test.vcap.me', '/')
+ NATS.start(:uri => @nats_server.uri) do
+ # Unregistration Message
+ NATS.publish('router.unregister', r.to_json) { NATS.stop }
+ end
+
+ # We should be unregistered here..
+ # Send out simple request and check request and response
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(req, 0)
+ response = rs.read(VCAP_NOT_FOUND.bytesize)
+ response.should == VCAP_NOT_FOUND
+ end
+ end
+
+ it 'should properly distibute messages between multiple backends' do
+
+ NUM_APPS = 10
+ NUM_REQUESTS = 100
+
+ apps = []
+
+ r = {:dea => '1234', :host => '127.0.0.1', :port => 0, :uris => ['lb_test.vcap.me']}
+
+ NATS.start(:uri => @nats_server.uri) do
+ # Create 10 backends
+ (0...NUM_APPS).each do |i|
+ apps << new_app_socket
+ # Registration Message
+ r[:port] = apps[i][1]
+ NATS.publish('router.register', r.to_json)
+ end
+ NATS.publish('done') { NATS.stop } # Flush through nats
+ end
+
+ req = simple_http_request('lb_test.vcap.me', '/')
+
+ app_sockets = apps.collect { |a| a[0] }
+
+ (0...NUM_REQUESTS).each do
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(req, 0)
+ end
+ end
+ sleep(0.25) # Wait here for requests to trip accept state
+ ready = IO.select(app_sockets, nil, nil, 1)
+ ready[0].should have(NUM_APPS).items
+ app_sockets.each { |s| s.close }
+
+ # Unregister
+ NATS.start(:uri => @nats_server.uri) do
+ (0...NUM_APPS).each do |i|
+ r[:port] = apps[i][1]
+ NATS.publish('router.unregister', r.to_json)
+ end
+ NATS.publish('done') { NATS.stop } # Flush through nats
+ end
+
+ end
+
+ it 'should properly do sticky sessions' do
+
+ apps = []
+ r = {:dea => '1234', :host => '127.0.0.1', :port => 0, :uris => ['sticky.vcap.me']}
+
+ NATS.start(:uri => @nats_server.uri) do
+ # Create 10 backends
+ (0...NUM_APPS).each do |i|
+ apps << new_app_socket
+ # Registration Message
+ r[:port] = apps[i][1]
+ NATS.publish('router.register', r.to_json)
+ end
+ NATS.publish('done') { NATS.stop } # Flush through nats
+ end
+
+ vcap_id = app_socket = nil
+ app_sockets = apps.collect { |a| a[0] }
+
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(STICKY_REQUEST, 0)
+ ready = IO.select(app_sockets, nil, nil, 1)
+ ready[0].should have(1).items
+ app_socket = ready[0].first
+ ss = app_socket.accept_nonblock
+ req_received = ss.recv(STICKY_REQUEST.bytesize)
+ req_received.should == STICKY_REQUEST
+ # Send a response back.. This will set the sticky session
+ ss.send(STICKY_RESPONSE, 0)
+ response = rs.read(STICKY_RESPONSE.bytesize)
+ # Make sure the __VCAP_ID__ has been set
+ response =~ /Set-Cookie:\s*__VCAP_ID__=([^;]+);/
+ (vcap_id = $1).should be
+ end
+
+ cookie = "__VCAP_ID__=#{vcap_id}"
+ sticky_request = simple_sticky_request('sticky.vcap.me', '/sticky', cookie)
+
+ # Now fire off requests, all should go to same socket as first
+ (0...5).each do
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(sticky_request, 0)
+ end
+ end
+
+ ready = IO.select(app_sockets, nil, nil, 1)
+ ready[0].should have(1).items
+ app_socket.should == ready[0].first
+
+ # Unregister (FIXME) Fails DRY
+ NATS.start(:uri => @nats_server.uri) do
+ (0...NUM_APPS).each do |i|
+ r[:port] = apps[i][1]
+ NATS.publish('router.unregister', r.to_json)
+ end
+ NATS.publish('done') { NATS.stop } # Flush through nats
+ end
+
+ # Check that is is gone
+ TCPSocket.open('127.0.0.1', RouterServer.port) do |rs|
+ rs.send(STICKY_REQUEST, 0)
+ response = rs.read(VCAP_NOT_FOUND.bytesize)
+ response.should == VCAP_NOT_FOUND
+ end
+
+ app_sockets.each { |s| s.close }
+
+ end
+
+end
142 spec/functional/spec_helper.rb
@@ -0,0 +1,142 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'nats/client'
+require 'yajl/json_gem'
+require 'vcap/common'
+require 'openssl'
+require 'net/http'
+require 'uri'
+
+require 'pp'
+
+class NatsServer
+
+ TEST_PORT = 4228
+
+ def initialize(uri="nats://localhost:#{TEST_PORT}", pid_file='/tmp/nats-router-tests.pid')
+ @uri = URI.parse(uri)
+ @pid_file = pid_file
+ end
+
+ def uri
+ @uri.to_s
+ end
+
+ def server_pid
+ @pid ||= File.read(@pid_file).chomp.to_i
+ end
+
+ def start_server
+ return if NATS.server_running? @uri
+ %x[ruby -S bundle exec nats-server -p #{@uri.port} -P #{@pid_file} -d 2> /dev/null]
+ NATS.wait_for_server(@uri) # New version takes opt_timeout
+ end
+
+ def is_running?
+ NATS.server_running? @uri
+ end
+
+ def kill_server
+ if File.exists? @pid_file
+ %x[kill -9 #{server_pid}]
+ %x[rm #{@pid_file}]
+ end
+ end
+end
+
+class RouterServer
+
+ PID_FILE = '/tmp/router-test.pid'
+ CONFIG_FILE = '/tmp/router-test.yml'
+ LOG_FILE = '/tmp/router-test.log'
+ PORT = 2228
+
+ def initialize(nats_uri)
+ port = "port: #{PORT}"
+ mbus = "mbus: #{nats_uri}"
+ log_info = "log_level: DEBUG\nlog_file: #{LOG_FILE}"
+ @config = %Q{#{port}\ninet: 127.0.0.1\n#{mbus}\n#{log_info}\npid: #{PID_FILE}}
+ end
+
+ def self.port
+ PORT
+ end
+
+ def server_pid
+ File.read(PID_FILE).chomp.to_i
+ end
+
+ def start_server
+ return if is_running?
+
+ # Write the config
+ File.open(CONFIG_FILE, 'w') { |f| f.puts "#{@config}" }
+
+ # Wipe old log file, but truncate so running tail works
+ if (File.exists? LOG_FILE)
+ File.truncate(LOG_FILE, 0)
+ # %x[rm #{LOG_FILE}] if File.exists? LOG_FILE
+ end
+
+ server = File.expand_path(File.join(__FILE__, "../../../bin/router"))
+ # pid = Process.fork { %x[#{server} -c #{CONFIG_FILE} 2> /dev/null] }
+ pid = Process.fork { %x[#{server} -c #{CONFIG_FILE} ] }
+ Process.detach(pid)
+
+ wait_for_server
+ end
+
+ def is_running?
+ require 'socket'
+ s = TCPSocket.new('localhost', PORT)
+ s.close
+ return true
+ rescue
+ return false
+ end
+
+ def wait_for_server(max_wait = 5)
+ start = Time.now
+ while (Time.now - start < max_wait) # Wait max_wait seconds max
+ break if is_running?
+ sleep(0.2)
+ end
+ end
+
+ def kill_server
+ if File.exists? PID_FILE
+ %x[kill -9 #{server_pid}]
+ %x[rm #{PID_FILE}]
+ end
+ %x[rm #{CONFIG_FILE}] if File.exists? CONFIG_FILE
+ end
+end
+
+
+# HTTP REQUESTS / RESPONSES
+
+FOO_HTTP_RESPONSE = "HTTP/1.1 200 OK\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: 53\r\nConnection: keep-alive\r\nServer: thin 1.2.7 codename No Hup\r\n\r\n<h1>Hello from the Clouds!</h1>"
+
+VCAP_NOT_FOUND = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\nVCAP ROUTER: 404 - DESTINATION NOT FOUND\r\n"
+
+STICKY_REQUEST = "GET /sticky HTTP/1.1\r\nHost: sticky.vcap.me\r\nConnection: keep-alive\r\nAccept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\n\r\n"
+
+STICKY_RESPONSE = "HTTP/1.1 200 OK\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: 242\r\nSet-Cookie: _session_id=be009e56c7be0e855d951a3b49e288c98aa36ede; path=/\r\nSet-Cookie: JSESSIONID=be009e56c7be0e855d951a3b49e288c98aa36ede; path=/\r\nConnection: keep-alive\r\nServer: thin 1.2.7 codename No Hup\r\n\r\n<h1>Hello from the Cookie Monster! via: 10.0.1.222:35267</h1><h2>session = be009e56c7be0e855d951a3b49e288c98aa36ede</h2><h4>Cookies set: _session_id, JSESSIONID<h4>Note: Trigger new sticky session cookie name via ?ss=NAME appended to URL</h4>"
+
+
+def simple_http_request(host, path, http_version='1.1')
+ "GET #{path} HTTP/#{http_version}\r\nUser-Agent: curl/7.19.7 (i486-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15\r\nHost: #{host}\r\nAccept: */*\r\n\r\n"
+end
+
+def simple_sticky_request(host, path, cookie, http_version='1.1')
+ "GET #{path} HTTP/#{http_version}\r\nHost: #{host}\r\nConnection: keep-alive\r\nAccept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\nCookie: #{cookie}\r\n\r\n"
+end
+
+def new_app_socket
+ app_socket = TCPServer.new('127.0.0.1', 0)
+ app_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
+ Socket.do_not_reverse_lookup = true
+ app_port = app_socket.addr[1]
+ [app_socket, app_port]
+end
3  spec/spec_helper.rb
@@ -0,0 +1,3 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+$:.unshift File.join(File.dirname(__FILE__), '..')
+$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
122 spec/unit/router_spec.rb
@@ -0,0 +1,122 @@
+# Copyright (c) 2009-2011 VMware, Inc.
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'router/router'
+require 'logger'
+
+module VCAP
+ class Component
+ class << self
+ attr_reader :varz
+ def setup
+ @varz = {}
+ end
+ end
+ end
+end
+
+describe Router do
+
+ before :all do
+ Router.config({})
+ VCAP::Component.setup
+ VCAP::Component.varz[:urls] = 0
+ VCAP::Component.varz[:droplets] = 0
+ end
+
+ describe 'Router.config' do
+ it 'should set up a logger' do
+ Router.log.should be_an_instance_of(Logger)
+ end
+
+ it 'should set up a session key' do
+ Router.session_key.should be
+ end
+
+ end
+
+ describe 'Router.register_droplet' do
+
+ it 'should register a droplet' do
+ Router.register_droplet('foo.vcap.me', '10.0.1.22', 2222)
+ VCAP::Component.varz[:droplets].should == 1
+ VCAP::Component.varz[:urls].should == 1
+ end
+
+ it 'should allow proper lookup' do
+ droplets = Router.lookup_droplet('foo.vcap.me')
+ droplets.should be_instance_of Array
+ droplets.should have(1).items
+
+ droplet = droplets.first
+ droplet.should have_key :url
+ droplet.should have_key :timestamp
+ droplet.should have_key :requests
+ droplet.should have_key :host
+ droplet.should have_key :port
+ droplet.should have_key :clients
+ droplet[:connections].should == []
+ droplet[:requests].should == 0
+ droplet[:url].should == 'foo.vcap.me'
+ droplet[:host].should == '10.0.1.22'
+ droplet[:port].should == 2222
+ droplet[:clients].should == {}
+ end
+
+ it 'should count droplets independent of URL' do
+ Router.register_droplet('foo.vcap.me', '10.0.1.22', 2224)
+ VCAP::Component.varz[:droplets].should == 2
+ VCAP::Component.varz[:urls].should == 1
+ end
+
+ it 'should return multiple droplets for a url when they exist' do
+ droplets = Router.lookup_droplet('foo.vcap.me')
+ droplets.should be_instance_of Array
+ droplets.should have(2).items
+ end
+
+ it 'should ignore duplicates' do
+ Router.register_droplet('foo.vcap.me', '10.0.1.22', 2224)
+ VCAP::Component.varz[:droplets].should == 2
+ VCAP::Component.varz[:urls].should == 1
+ end
+
+ end
+
+ describe 'Router.unregister_droplet' do
+ it 'should unregister a droplet' do
+ Router.unregister_droplet('foo.vcap.me', '10.0.1.22', 2224)
+ VCAP::Component.varz[:droplets].should == 1
+ VCAP::Component.varz[:urls].should == 1
+ end
+
+ it 'should not return unregistered items' do
+ Router.unregister_droplet('foo.vcap.me', '10.0.1.22', 2222)
+ droplets = Router.lookup_droplet('foo.vcap.me')
+ droplets.should be_nil
+ end
+
+ it 'should properly account for urls and droplets' do
+ VCAP::Component.varz[:droplets].should == 0
+ VCAP::Component.varz[:urls].should == 0
+ end
+ end
+
+ describe 'Router.session_keys' do
+ it 'should properly encrypt and decrypt session keys' do
+ Router.register_droplet('foo.vcap.me', '10.0.1.22', 2222)
+ droplets = Router.lookup_droplet('foo.vcap.me')
+ droplets.should have(1).items
+ droplet = droplets.first
+ key = Router.generate_session_cookie(droplet)
+ key.should be
+ droplet_array = Router.decrypt_session_cookie(key)
+ droplet_array.should be_instance_of Array
+ droplet_array.should have(3).items
+ droplet_array[0].should == droplet[:url]
+ droplet_array[1].should == droplet[:host]
+ droplet_array[2].should == droplet[:port]
+ end
+end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.