Go Back to Redneck Programmer

http_load_tester.rb

What it is? A simple Ruby script to kind of simulate multiple users hitting a web site or web application. You pass in the number of threads (simultaneous users) and a name of a text file which contains 1 URL per line.

What it is NOT? Robust or "accurate". It's a simple hack -- it starts up the number of threads you tell it to, but they are Ruby threads, not "real" OS-level threads. Each thread loops through the array of URL's in the text file and retrieves them. It is a simple HTML retrieval though, not a true simulation of a real browser. This pulls the HTML, will follow redirects, etc, but then chucks the response and moves on. A web browser would retieve the HTML, parse it looking for external resources (CSS, images, JS files, etc) and then retieve those as well, then render it all and run any embedded client side scripts. This does none of that. (unless those URL's are in your text file, I guess)

I find it useful for simple tests and simulating light loads, but don't use it to verify it pulls "good" info, and don't use it to test responsiveness of the webserver. Mostly just use it to monitor open sessions, database connections, memory usage, etc on the server as volume of request ramps up.

The source code is at http_load_tester.rb

#!/usr/bin/env ruby
require 'net/http'
require 'uri'

## very basic single-page fetcher, will follow redirection but only fetches initial response
## does not go back and fetch linked resources like css, images, js files, etc.
class HttpFetcher 
  
  def fetch2(uri_str, limit = 10)
    raise ArgumentError, 'HTTP redirect too deep' if limit == 0
    response = Net::HTTP.get_response(URI.parse(uri_str))
    case response
    when Net::HTTPSuccess     then 
      response 
      puts "success "
    when Net::HTTPRedirection then 
      print "redirect "
      fetch2(response['location'], limit - 1)
    when Net::HTTPClientError then
      response 
      puts "client error"
    when Net::HTTPServerError then
      response 
      puts "server error"
    else
      response.error!
    end
    #puts "** NEW REQUEST\n#{response.body}"    
  end  
end

## this is what ramps up the requests to multple threads
## each thread looping through and fetching each url in the array once
## future enhancement might be pass in param for number of times each thread runs through array of URL's
class HttpLoadRunner
  
  def fetch_in_threads(uri_strs, num_threads)
    $cnt = 0
    tt = []    
    num_threads.times  do
      $cnt += 1
      thread = Thread.new do
        begin
          x = HttpFetcher.new
          uri_strs.each do |uri_str|
            puts "Thread #{$cnt} ** #{uri_str} ** starting"
            x.fetch2(uri_str, 2)
            puts "Thread #{$cnt} ** #{uri_str} ** done"
          end
        rescue
          puts "Exception"
        end
      end
      tt << thread
    end
    tt.each {|thr| thr.join }
  end
  
end

## next line checks to see if this script is being run itself or run as library
## only runs this tuff if standalone
if __FILE__ == $0  
  if ARGV.length != 2 then
    puts "Usage: #{$0} num-threads url-file"
    puts "\twhere num-threads is an integer indicating number of simulated users (threads)"
    puts "\tand url-file is path/filename to a text file containing 1 URL per line"	
    exit
  end
  
  numthreads = (ARGV.first == nil) ? 1 : ARGV.first.to_i
  puts "Number of threads to run: #{numthreads}"
  
  filename = ARGV[1]
  puts "URL filename: #{filename}"
  
  test_urls = []
  File.new(filename, "r").each { |line| test_urls << line }
  puts "URLS:\n -> #{test_urls.join("\n -> ")}"
  
  lr = HttpLoadRunner.new
  lr.fetch_in_threads(test_urls, numthreads)
end