![]()
Include('app_page.inc', $args, $html); } sub app::navbar { print "NAVBAR STUB" }
app_page.inc
<% my($args, $body) = @_; %>
Global Inc. Portal: <%= $args->{title} %>  | Global Inc. Portal Site
<%= time2str ("%B %e, %Y", time) %> | |
| | | <%= $body || 'BODY STUB' %> | |
| | Global Inc., San Diego CA, 99999 contact@globalinc.com.fake |
XMLSub app:page
And now for our first engineered web page:
index.asp
To finish the Home Page we must construct the
and fill in the page's contents.
XMLSub app:navbar
global.asa
use File::Basename qw(basename); my %Pages = ('index' => 'Home', 'links' => 'Links', 'partners' => 'Partners' ); sub app::navbar { my($args) = @_; my $size = $args->{size} || 2; my $basename = basename($0); my @links = split(/,/, $args->{links} || 'index,links,partners'); print "
"; for my $page (@links) { my $title = uc $Pages{$page}; my $text = ($basename =~ /$page/) ? $title : qq($title); print qq{| $text | }; } print "
"; }
Home Page
Extend
and reuse!
Welcome to Global Inc. portal site, where you can get Global's latest links, and explore the web with Global's partners, and contact Global.
XMLSub app:bigbox
We build a
, which we find to be composed of two smaller
tags.
global.asa
sub app::bigbox { my($args, $html) = @_; $main::Response->Include('app_bigbox.inc', $args, $html); }
app_bigbox.inc
<% my($args, $body) = @_; %>
<%= uc($args->{title}) || "NEEDS TITLE" %>
<%= $body %>
global.asa
sub app::borderbox { my($args, $html) = @_; $main::Response->Include('app_borderbox.inc', $args, $html);
}
app_borderbox.inc
<% my($args, $body) = $args->{outer} ||= $args->{inner} ||= $args->{width} ||= @_; 'red'; 'white'; '100%';
%>
XMLSub app:bigbox
is done.
partners.html
[ partner links here ]
XMLSub app:partner
we aliased to
.... Mark up content now, upgrade later.
global.asa
sub app::partner { my($args, $html) = @_; print qq(
$html); }
partners.html
Final Partner Page
QA for Round 2
Now is good time to QA for the 1st time:
l
l
l
l
3 pages of functional testing cross browser testing load testing beginning build of regression tests
QA Requirements
In order for QA to be effective they need separate but identical hardware & software.
l
l
Changes in dev won't affect QA until released Parallel QA to development progress
Identical components needed are:
l
l
l
l
physical hardware ( if possible ) perl installations apache + mod_perl installations Apache::ASP versions, and CPAN modules
QA needs to be like development environment. Production needs to exactly reproduce QA env.
QA Empowerment
QA needs full autonomy from developers.
l
l
stop code release from going to production possibly in charge of publishing processes
Needs developer man power too, especially for building QA regression tests. Developers as QA! Autonomy from, but compatible with developers. QA must often rely on developers for risk analysis & technical expertise.
QA Scheduling
No one idles! Have weekly, bi-weekly development cycles with firm code release dates on regular schedule. Non-regularity promotes errors making it to production. Concurrent development model, QA working on Round 2 code branch checked in last week, while developers work on Round 3 features.
QA Regression Testing
For testing web sites, functional regression testing done well by scripted web client. Good tools for this:
l
l
l
l
HTTP::WebTest, regression web testing LWP, HTTP programming LWP::UserAgent LWP::Simple lwp-request SilkTest from Segue Software Inc. New RoboWeb project looks promising. http://sourceforge.net/projects/roboweb
¡ ¡ ¡
Regression testing useful to developers, QA, and post production release verification.
Production Publishing
After Round 2 QA passes, OKs the publish. Customer not seeing until Round 3 is done, but production practice this period is good. Publishing tools:
l
Remote copy rsync: ssh secure, compression, changes only ftp: not useful for single box for dev/qa/prod
¡ ¡
l
Software Configuration Management (SCM) Production server is another developer workspace cvs: cvs update perforce: p4 sync
¡ ¡
Production Publishing Timing
How to rollout to production, with ability to recover... 1. backup current code in production 2. stop Apache httpd 3. publish changes 4. start Apache httpd 5. regression testing If regression tests fail:
l
l
l
l
stop Apache httpd restore backup start Apache httpd regression testing
This entire process can be automated.
Production Monitoring
Make sure your site stays online:
l
l
l
Periodic regression tests Ping specific test cases with LWP scripting Home page SSL page Database read transaction Database write transaction Outsource: Keynote Systems, advanced reporting & expensive NetMechanic affordable for URL monitoring.
¡ ¡ ¡ ¡ ¡ ¡
Monitoring typically provides means to notify person on call in realtime.
Production Realtime Error Notification
Apache::ASP realtime error notification:
# set SMTP host PerlSetVar MailHost your.smtp.server.hostname # set destination email for receipt of full error message # plus debugging data PerlSetVar MailErrorsTo youremail@yourdomain.com # send small alert, suitable for pager to this address # whenever error occurs, 1 per MailAlertPeriod PerlSetVar MailAlertTo youremail@yourdomain.com # Period in minutes during which only one MailAlert # may be sent for errors at the web site PerlSetVar MailAlertPeriod 20
Production Security, Don't Get Hacked
No point deploying web site on hostile internet, if don't secure your servers. Firewall Security:
l
l
l
ipchains for Linux IP Filter for other platforms Commercial solutions abound, e.g. Checkpoint
Host level security:
l
l
l
OpenBSD, supposedly very secure base install Bastille Linux to harden distributions Tripwire, IDS for ongoing maintenance Know when you have been hacked!
Round 3, Implementation
In Round 2, we engineered the web site with good design principles. No new features. No customer feedback needed. Now we implement the XML news features, which Apache::ASP will support well out of the box. Steps: 1. Administrative Login 2. XML News File Upload Tool 3. XML News Rendering Page
Adminstrative Login
The admin login page functionality can be broken into 3 parts:
l
l
l
Admin Login Page Database Connection Process Login Credentials
Administrative Login Page
Allow for deeper XMLSubs packages...
PerlSetVar XMLSubsMatch app:[\w:]+
and then create app:form tag space:
Admin Login Page XMLSubs
sub app::form::table { my($args, $html) = @_; my $basename = basename($0); print qq(
\n"; } sub app::form::row { my $args = shift; $args->{name} || die("need name for form row".join(%$args)); $args->{title} && ( $args->{title} .= ':' ); # NULL titles OK $args->{value} = $main::Server->HTMLEncode($args->{value}); print <
$args->{title} | | HTML ; }
Admin Login Page Screen
Database Connection
1. Connect to database in each Script_OnStart 2. Create $App object, store $Db & misc there
global.asa, Script_OnStart
use DBI; use vars qw($App); sub Script_OnStart { $main::App = $app::App = $App = bless { Db => DBI->connect('dbi:mysql:hostname=mysql;database=test', 'test', 'test', { RaiseError => 1 }), Form => $Request->Form, }; } { shift->{Form}{username} }; sub username { shift->{Form}{password} }; sub password { shift->{Form}{login} }; sub login sub login_done { $Response->Redirect('admin_login.asp'); }
Process Login Credentials
Add XMLSubs method to check for login credentials.
admin.asp, top
global.asa, app:login
sub app::login { if($App->login) { my $pass_csr = $App->{Db} ->prepare("select username from passwords ". "where username = ? ". "and password = ?"); $pass_csr->execute($App->username, $App->password); my $valid_user = $pass_csr->fetchrow_arrayref; if($valid_user) { $main::Session->{login} = 1; $App->login_done; } } }
Form Fill
To have the form redraw itself with the user input on password verification error, just set:
PerlSetVar FormFill 1
and have already installed HTML::FillInForm.
XML News File Upload Page
global.asa, app:upload:table tag
sub app::upload::table { my($args, $html) = @_; my $basename = basename($0); print qq(
\n"; }
admin_login.asp, file upload
New File Upload Format
The agreed format for the news file upload looks like:
news.xml
This is news! 2001-07-23 The news article text here. Something very special happened today, and we are going to tell you about it now. This is news too! 2001-07-22 The other news article text here. Something else news worthy happened.
File Upload Process
Add a
tag to the admin_login.asp script whereever you want success/error output to be returned for the upload.
=>
File Upload Process
Use full XML parser, like XML::Simple to deal better with deep XML structures.
Apache::ASP XMLSubs better at rendering flat XML structures.
global.asa, app:upload:process tag
use XML::Simple; sub app::upload::process { my $Request = $main::Request; if($Request->{FileUpload}{upload_file}) { local $SIG{__DIE__} = 'die'; my $fh = $Request->{FileUpload}{upload_file}{FileHandle}; my $data = join('', <$fh>); my $error; my $ref = eval { &XMLin($data); }; $@ && ($error = "Error in XML: $@"); if($ref && ! $ref->{article}) { $error = "There were no articles found in the upload!"; } if($error) { print "
$error";
return; } open(FILE, ">news.xml") || die("no write access to news.xml: $!"); print FILE $data; close FILE; print "News Upload Success!"; } }
News Page
Don't always decompose code from templates into perl libraries. Motivations for decomposing templates include:
l
l
code reuse template readability for HTML developers
Example here to not remove all code possible.
news.asp
<% if(! -e 'news.xml') { %> No news posted yet. <% } else { my $news = XMLin(); %>
|
| GLOBAL NEWS |
<% for my $article ( @{$news->{article}} ) { %>
|
| <%= $article->{title} %> | <%= $article->{date} %> |
| <%= $article->{body} %> |
<% } %>
<% } %>
News Page
Date Stamp
The customer also wanted a date stamp on the pages like July 23, 2001. First, we load the Date::Format module in global.asa:
global.asa
use Date::Format qw(time2str);
Then we add some date code below the header:
app_page.inc
<%= time2str ("%B %e, %Y", time) %>
ALL pages update, since all used the
tag.
Maintenance, Source Control
Big development items done, now in maintenance mode for customer. To make source management nicer, get a code release policy consistent with the tool you are using like CVS or Perforce. Then make sure, you have code split up logically into lots of files. Reduce inter-developer source change collisions.
Maintenance, Source Control
The prior examples placed a lot of code in global.asa. Leave only in global.asa:
l
l
ASP event handlers "use module" declarations, especially to import module exports into scripts and includes.
Thus, global.asa would likely get split up into:
l
l
l
l
l
global.asa Site/App.pm - for the $App object, init as Site::App->new() app.pm - for the app:* tags app/form.pm - for the app:form:* tags app/upload.pm - for the app:upload:* tags
Scaling to 1,000,000 Users per Day
What is 1,000,000 users per day? 10 clicks per user? Most hits average across 12 hour period ( assume ), so the rate that is needed to be handled is:
1,000,000 users/day x 10 hits/user ---------------------------------- = 231 hits / sec. 12 hours x 3600 seconds/hour
OK, let's plan for double capacity, an even 460 hits/sec that we want to handle.
Scaling to 1,000,000 Users
How fast is application currently? Take home page speed test of index.asp using ab, ApacheBench.
]# ab -c 5 -n 500 http://gate/asp/apps/ginc/index.asp This is ApacheBench, Version 1.3c <$Revision: 1.38 $> apache-1.3 Time taken for tests: 16.751 seconds Requests per second: 29.85 Transfer rate: 63.55 kb/s received
I can get 30 hits/sec on a PIII dual 400 with $Session active. A dual 1GHz processor would likely be 2.5 times faster, at 75 hits per sec. So we need a web cluster of 6 machines with dual 1GHz CPUs, and knowing how mod_perl eats up RAM, probably 1G RAM each box too!
Web Clustering
The clustering is not native to Apache::ASP. Must use front end web load balancers like:
l
l
l
l
Linux Virtual Server Project Round Robin DNS with short TTL's Alteon Web Systems Products Cisco Local Director
... just to name a few.
$Session Clusters with Apache::Session
If using $Session, cannot cluster with hot failover or fault tolerance because the Apache::ASP $Session files are on local disk. Work-around is to use Apache::Session to store session files to a central database, like this:
global.asa
use Apache::Session::MySQL; sub Script_OnStart { # ... init mysql dbh before my $id = $Request->Cookies("SESSION_ID"); tie %hash, 'Apache::Session::MySQL', $id, { Handle => $dbh, LockHandle => $dbh }; if($id ne $hash{_session_id}) { $Response->Cookies("SESSION_ID", $hash{_session_id}); } $Session = \%hash; $Server->RegisterCleanup(sub { untie (%$Session) }); }
$Session Clustering, smaller cluster
With PerlSetVar NoState 1 set, disabling $Session and $Application, performance goes to 120 hits/sec on my 2x400 PIII Linux server. Likely drop to 100 hits/sec with Apache::Session overhead, but with sessions centralized at database, cluster will likely shrink to 2 web servers with dual 1 GHz CPUs, with 1 dual 1GHz CPU MySQL database serving as the $Session database. Cluster shrinks from 6 to 3 servers. MySQL new single point of failure and potential bottleneck for further scaling.
Apache::Session Drawbacks
Apache::Session does not support:
l
l
l
Session_OnStart and Session_OnEnd ASP events No Session Manager to garbage collect stale sessions Good API for $Application construction.
Future work on Apache::ASP will include support for $Session & $Application storage in database, but will be slower than a raw Apache::Session $Session because of support for the above features.
THE END