Dispense: kata-cgi-quotes

Matteo Vaccari > Applicazioni Web

The cgi-quotes kata

Prerequisites:

Ingredients: a fortune file (for instance this one or another one found on the Internet).

Summary

Prologue

Open your browser at http://localhost/cgi-bin/quotes.cgi . Observe the 404 error.

Find the cgi directory on your computer. On Linux, it’s usually /var/www/cgi-bin. On Mac OS X, it’s usually /Library/WebServer/CGI-Executables. Create there a file named quotes.cgi, with contents:

#!/usr/bin/env ruby

print "Content-Type: text/plain\r\n"
print "\r\n"

print "Hello, world!"

Open again reload http://localhost/cgi-bin/quotes.cgi in the browser. Observe the 500 error. What is the reason? Find the error log. It’s usually at /var/log/apache2/error_log. It’s not clear which lines were produced by our script, so we do

  
$ tail -f /var/log/apache2/error_log

type a few returns, and reload the browser. Now it’s clear

[Wed Nov 26 21:42:50 2008] [error] [client ::1] (13)Permission denied: exec of '/Library/WebServer/CGI-Executables/quotes.cgi' failed
[Wed Nov 26 21:42:50 2008] [error] [client ::1] Premature end of script headers: quotes.cgi

Add the executability bit and reload the browser

$ chmod +x /Library/WebServer/CGI-Executables/quotes.cgi

Now we observe the “Hello, world!” message.

Open a second terminal window, and tail the access_log file. Reload the browser and observe the log message. Observe the success status 200.

Showing quotes

Copy the fortune file to /tmp/quotes.txt. Change quotes.cgi to

#!/usr/bin/env ruby
print "Content-Type: text/plain\r\n"
print "\r\n"

print File.open("/tmp/quotes.txt").read

Reload the browser.

Now change quote.cgi to

#!/usr/bin/env ruby
print "Content-Type: text/plain\r\n"
print "\r\n"

quotes = File.open("/tmp/quotes.txt").read.split("%")
print quotes[33]

Reload the browser. Observe we see only one quote now.

Change the last lines to

quotes = File.open("/tmp/quotes.txt").read.split("%")
print quotes[rand(quotes.size)]

Reload the browser repeatedly. Observe we see a different quote every time.

Converting to html.

Rewrite quotes.cgi to

#!/usr/bin/env ruby
print "Content-Type: text/html\r\n"
print "\r\n"

quotes = File.open("/tmp/quotes.txt").read.split("%")
quote = quotes[rand(quotes.size)]

print "
<html>
  <head><title>Quote of the day</title></head>
  <body>
    <h3>Quote of the day</h3>
    <pre>
    #{quote}
    </p e>
  </body>
</html>
"

Reload the browser. Observe that the browser window title is changed as well.

Reading parameters

We want to select all quotes that contain a given string. Point the browser to http://localhost/cgi-bin/quotes.cgi?q=Linus. Observe no difference from before. Now change quotes.cgi to

#!/usr/bin/env ruby
require 'cgi'

print "Content-Type: text/html\r\n"
print "\r\n"
quotes = File.open("/tmp/quotes.txt").read.split("%")

cgi = CGI.new
keyword = cgi['q']
if keyword
  quote = keyword
else
  quote = quotes[rand(quotes.size)]
end
... 

Reload the browser. Observe that the “q” parameter appears in place of the quote. Try to remove the parameter and see that the old behaviour is no longer working. Try replacing text/html to text/plain and observe that we get an empty quote. This means that the cgi object returns empty strings for missing parameters, and empty strings are not false. Correct by changing if keyword to if keyword.length > 0. Observe that the behaviour when the q parameter is missing is now back to what it was before.

Now change the code to

quote = quotes.find {|quote| quote.include?(keyword) }

Reload the browser. Change the query string, and observe different quotes are retrieved. For any given value of q, always the same quote is retrieved. Randomize by changing the code to

quote = quotes.select {|quote| quote.include?(keyword) }

Reload with q=Linus and see an array of matching quotes.

Now change the code to

if keyword.length > 0
  quotes = quotes.select {|quote| quote.include?(keyword) }
end
quote = quotes[rand(quotes.size)]

And see the selected quote change randomly.

Measure performance

Execute

  
$ ab -i -n 1000 -c   1 http://localhost/cgi-bin/quotes.cgi
$ ab -i -n 1000 -c  10 http://localhost/cgi-bin/quotes.cgi
$ ab -i -n 1000 -c 100 http://localhost/cgi-bin/quotes.cgi

And produce a table of the mean response time, and maximum response time in 90% of the cases, versus concurrency level.

Concurrency Mean response time (ms) Response time for 90% (ms) Requests per second
1 6 6 160
10 38 58 263
100 372 398 268