Changeset 89

Show
Ignore:
Timestamp:
08/15/06 18:22:35 (2 years ago)
Author:
bchoate
Message:

Added synchronization configuration option (support for synchronization can be enabled/disabled). Added support for syncing uploaded files. Prevent rebuild queue from queueing when app mode is an explicit rebuild* mode.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/RebuildQueue/README.txt

    r86 r89  
    1717Requirements 
    1818============ 
     19 
     20Movable Type version 3.3 or later (or Movable Type Enterprise). 
    1921 
    2022The 'rsync' command if using remote server synchronization. 
  • trunk/RebuildQueue/plugins/RebuildQueue/RebuildQueue.pl

    r85 r89  
    2727    my $help      = 0; 
    2828    my %throttle; 
    29     my $worker    = 1; 
     29    my @worker; 
     30    my $worker    = ''; 
    3031    my $sync      = 0; 
    3132    my $rsync_opt = ''; 
     
    3738        "help|?"      => \$help, 
    3839        "throttle=i"  => \%throttle, 
    39         "worker=i"    => \$worker, 
     40        "worker=s"    => \$worker, 
    4041        "sync"        => \$sync, 
    4142        "to|target=s" => \@target, 
     
    6364        } 
    6465    } 
     66    if ($worker =~ m/,/) { 
     67        @worker = split(/,/, $worker); 
     68    } else { 
     69        @worker = ($worker) if $worker =~ m/^\d+$/; 
     70    } 
    6571 
    6672    require RebuildQueue::Daemon; 
    6773    if ($sync) { 
    6874        RebuildQueue::Daemon->new->sync( 
    69             worker    => $worker, 
     75            daemonize => $daemonize, 
     76            'sleep'   => $sleep, 
     77            worker    => \@worker, 
    7078            target    => \@target, 
    7179            rsync_opt => $rsync_opt, 
     
    7684            'sleep'   => $sleep, 
    7785            throttle  => \%throttle, 
    78             worker    => $worker, 
     86            worker    => \@worker, 
    7987        ); 
    8088    } 
  • trunk/RebuildQueue/plugins/RebuildQueue/lib/RebuildQueue/Daemon.pm

    r85 r89  
    3939    my (%opt) = @_; 
    4040 
    41     my $worker    = $opt{worker}    || 1; 
     41    my $nap_time  = $opt{'sleep'}   || 5; 
     42    my $daemonize = $opt{daemonize} || 0; 
     43    my $worker    = $opt{worker}    || []; 
    4244    my $targets   = $opt{target}    || []; 
    4345    my $rsync_opt = $opt{rsync_opt} || '-a'; 
    4446 
    45     unless ($unlock = $mt->_lock("sync-$worker")) { 
     47    # We need the plugin for getting settings, but not for anything 
     48    # else... 
     49    require RebuildQueue::Plugin; 
     50    my $plugin = RebuildQueue::Plugin->instance; 
     51    $plugin->disable; 
     52 
     53    my $sync_support = $plugin->sync_support; 
     54    unless ($sync_support) { 
     55        print "Synchronization is not enabled.\n"; 
    4656        exit 1; 
    4757    } 
    4858 
    49     local $SIG{INT} = sub { die }; 
    50  
    51     print "RebuildQueue sync running...\n"; 
    52     my $sync_start = [gettimeofday]; 
    53  
    54     my $iter = RebuildQueue::File->load_iter({ 
    55         sync_me => 1, worker => $worker 
    56     }, { 'sort' => 'priority', direction => 'ascend' }); 
    57     my @rqf; 
    58     my @files; 
    59     while (my $rqf = $iter->()) { 
    60         my $fi = $rqf->fileinfo; 
    61         push @files, $fi->file_path if $fi && (-f $fi->file_path); 
    62         push @rqf, $rqf; 
    63     } 
    64     my $synced = 0; 
    65     if (@files) { 
    66         $synced = scalar @files; 
    67         require File::Spec; 
    68         my $file = File::Spec->catfile($mt->config('TempDir'), "rebuildq-rsync-$$.lst"); 
    69         open FOUT, ">$file"; 
    70         print FOUT join("\n", @files) . "\n"; 
    71         close FOUT; 
    72         foreach my $target (@$targets) { 
    73             my $cmd = "rsync $rsync_opt --files-from=\"$file\" / \"$target\""; 
    74             my $res = system $cmd; 
    75             my $exit = $? >> 8; 
    76             if ($exit != 0) { 
    77                 print STDERR "Error during rsync of files in $file...\n"; 
    78                 print STDERR "Command: $cmd\n"; 
    79                 print STDERR $res; 
    80                 exit 1; 
    81             } 
    82         } 
    83         unlink $file; 
    84         # clear sync flags... 
    85         $_->remove foreach @rqf; 
    86     } 
    87     if ($synced) { 
    88         print "RebuildQueue sync finished. ($synced files in " . sprintf("%0.02f", tv_interval($sync_start)) . " seconds)\n"; 
    89     } else { 
    90         print "No files available to sync.\n"; 
    91     } 
     59    my $worker_id = join ',', sort @$worker; 
     60    $worker_id = 'all' unless $worker_id; 
     61    unless ($unlock = $mt->_lock("sync-$worker_id")) { 
     62        exit 1; 
     63    } 
     64 
     65    my $stop = 0; 
     66    local $SIG{INT}  = sub { $stop = 1 }; 
     67    local $SIG{QUIT} = sub { $stop = 1 }; 
     68 
     69    $| = 1; 
     70 
     71    print "RebuildQueue sync daemon running...\n" if $daemonize; 
     72 
     73    while (!$stop) { 
     74        my $sync_set = [gettimeofday]; 
     75        my $iter = RebuildQueue::File->load_iter({ 
     76            sync_me => 1, (@$worker ? ( worker => $worker ) : ()), 
     77        }, { 'sort' => 'priority', direction => 'ascend' }); 
     78        my @rqf; 
     79        my @files; 
     80        my @static_fileinfo; 
     81        while (my $rqf = $iter->()) { 
     82            my $fi = $rqf->fileinfo; 
     83            push @files, $fi->file_path if $fi && (-f $fi->file_path); 
     84            push @rqf, $rqf; 
     85            unless ($fi->template_id) { 
     86                # static file 
     87                push @static_fileinfo, $fi; 
     88            } 
     89        } 
     90        my $synced = 0; 
     91        if (@files) { 
     92            $synced = scalar @files; 
     93            require File::Spec; 
     94            my $file = File::Spec->catfile($mt->config('TempDir'), "rebuildq-rsync-$$.lst"); 
     95            open FOUT, ">$file"; 
     96            print FOUT join("\n", @files) . "\n"; 
     97            close FOUT; 
     98            foreach my $target (@$targets) { 
     99                my $cmd = "rsync $rsync_opt --files-from=\"$file\" / \"$target\""; 
     100                my $res = system $cmd; 
     101                my $exit = $? >> 8; 
     102                if ($exit != 0) { 
     103                    # TBD: notification to administrator 
     104                    # At the very least, log to MT activity log. 
     105                    print STDERR "Error during rsync of files in $file...\n"; 
     106                    print STDERR "Command: $cmd\n"; 
     107                    print STDERR $res; 
     108                    exit 1; 
     109                } 
     110            } 
     111            unlink $file; 
     112            # clear sync flags... 
     113            $_->remove foreach @rqf; 
     114            $_->remove foreach @static_fileinfo; 
     115        } 
     116        if ($synced) { 
     117            print "RebuildQueue sync finished. ($synced files in " . sprintf("%0.02f", tv_interval($sync_set)) . " seconds)\n"; 
     118        } else { 
     119            print "No files available to sync.\n" unless $daemonize; 
     120        } 
     121        if (!$daemonize) { 
     122            last; 
     123        } 
     124        sleep $nap_time; 
     125    } 
     126    print "\nShutting down RebuildQueue...\n" if $daemonize; 
    92127} 
    93128 
     
    96131    my (%opt) = @_; 
    97132 
    98     my $nap_time = $opt{'sleep'} || 5; 
    99     my $daemonize = $opt{daemonize}; 
    100     my $throttles = $opt{throttle} || {}; 
    101     my $worker = $opt{worker} || 1; 
    102  
    103     unless ($unlock = $mt->_lock("daemon-$worker")) { 
     133    my $nap_time  = $opt{'sleep'}   || 5; 
     134    my $daemonize = $opt{daemonize} || 0; 
     135    my $throttles = $opt{throttle}  || {}; 
     136    my $worker    = $opt{worker}    || []; 
     137 
     138    my $worker_id = join ',', sort @$worker; 
     139    $worker_id = 'all' unless $worker_id; 
     140    unless ($unlock = $mt->_lock("daemon-$worker_id")) { 
    104141        # an existing daemon is running with this worker id. don't 
    105142        # allow a second to run. 
     
    107144    } 
    108145 
     146    # We need the plugin for getting settings, but not for anything 
     147    # else... 
     148    require RebuildQueue::Plugin; 
     149    my $plugin = RebuildQueue::Plugin->instance; 
     150    $plugin->disable; 
     151 
     152    my $sync_support = $plugin->sync_support; 
     153 
    109154    my $stop = 0; 
    110     local $SIG{INT} = sub { $stop = 1 }; 
     155    local $SIG{INT} = sub { $stop = 1 }; 
    111156    local $SIG{QUIT} = sub { $stop = 1 }; 
    112157 
     
    115160    $mt->cleanup_rebuild_queue; 
    116161 
    117     print "RebuildQueue daemon running...\n" if $daemonize; 
     162    print "RebuildQueue build daemon running...\n" if $daemonize; 
    118163 
    119164    my $pub = $mt->publisher; 
     
    121166    while (!$stop) { 
    122167        my $iter = RebuildQueue::File->load_iter({ 
    123             rebuild_me => 1, worker => $worker
     168            rebuild_me => 1, (@$worker ? ( worker => $worker ) : ())
    124169            build_time => [undef, time], 
    125170        }, { 
     
    168213                if ($mtime != $mtime2) { 
    169214                    # file was updated; mark for syncing 
    170                     $rqf->sync_me(1); 
    171                     $rqf->rebuild_me(0); 
    172                     $rqf->save; 
     215                    if ($sync_support) { 
     216                        $rqf->sync_me(1); 
     217                        $rqf->rebuild_me(0); 
     218                        $rqf->build_time(time); 
     219                        $rqf->save; 
     220                    } else { 
     221                        $rqf->remove; 
     222                    } 
    173223                } else { 
    174224                    # touch file to help throttle mechanism 
     
    195245} 
    196246 
     247# Method to remove any RebuildQueue::File objects that 
     248# are no longer requiring rebuild or sync operations. 
    197249sub cleanup_rebuild_queue { 
    198250    my $mt = shift; 
     
    204256} 
    205257 
     258# Summarizes the current queue item for display in the log. 
    206259sub _summary { 
    207260    my $fi = shift; 
     
    214267    $file =~ s/^\Q$root\E//; 
    215268    $file =~ s/^\///; 
     269    # TBD: Handle this situation more gracefully. 
    216270    die "failed to load template " . $fi->template_id unless $tmpl; 
    217271# Output summary 
  • trunk/RebuildQueue/plugins/RebuildQueue/lib/RebuildQueue/Plugin.pm

    r87 r89  
    1515use base 'MT::Plugin'; 
    1616 
    17 our $VERSION        = '1.0'; 
     17our $VERSION        = '1.01'; 
    1818our $SCHEMA_VERSION = '1.01'; 
    19  
    20 my $plugin; 
     19our $ENABLED = 1; 
     20 
     21my $plugin = new RebuildQueue::Plugin({ 
     22    name           => "Rebuild Queue", 
     23    description    => "Queues rebuild operations for offline building.", 
     24    object_classes => ['RebuildQueue::File'], 
     25    version        => $VERSION, 
     26    schema_version => $SCHEMA_VERSION, 
     27    author_name    => "Six Apart, Ltd.", 
     28    author_link    => "http://www.sixapart.com/", 
     29    plugin_link    => "http://code.sixapart.com/", 
     30    icon           => "rebuildq.gif", 
     31    system_config_template => \&system_config_template, 
     32    blog_config_template   => \&blog_config_template, 
     33    settings       => new MT::PluginSettings([ 
     34        ['workers',       { Default => 1,     Scope => 'system' }], 
     35        ['rebuildq_sync', { Default => 0,     Scope => 'system' }], 
     36        ['rebuildq_mode', { Default => 0,     Scope => 'blog'   }], 
     37        ['worker',        { Default => undef, Scope => 'blog'   }], 
     38    ]), 
     39    callbacks      => { 
     40        'BuildFileFilter' => { 
     41            priority => 10, 
     42            code     => \&build_file_filter, 
     43        }, 
     44        'BuildFile' => { 
     45            priority => 10, 
     46            code     => \&build_file, 
     47        }, 
     48        'MT::FileInfo::post_remove' => { 
     49            priority => 1, 
     50            code     => \&fileinfo_post_remove, 
     51        }, 
     52        'MT::FileInfo::post_remove_all' => { 
     53            priority => 1, 
     54            code     => \&fileinfo_post_remove_all, 
     55        }, 
     56        'CMSUploadFile' => { 
     57            priority => 1, 
     58            code     => \&cms_upload_file, 
     59        }, 
     60    }, 
     61}); 
    2162 
    2263if (!MT->instance->isa('RebuildQueue::Daemon')) { 
    23     $plugin = new RebuildQueue::Plugin({ 
    24         name           => "Rebuild Queue", 
    25         description    => "Queues rebuild operations for offline building.", 
    26         object_classes => ['RebuildQueue::File'], 
    27         version        => $VERSION, 
    28         schema_version => $SCHEMA_VERSION, 
    29         author_name    => "Six Apart, Ltd.", 
    30         author_link    => "http://www.sixapart.com/", 
    31         plugin_link    => "http://code.sixapart.com/", 
    32         icon           => "rebuildq.gif", 
    33         system_config_template => \&system_config_template, 
    34         blog_config_template   => \&blog_config_template, 
    35         settings       => new MT::PluginSettings([ 
    36             ['workers',       { Default => 1,     Scope => 'system' }], 
    37             ['rebuildq_mode', { Default => 0,     Scope => 'blog'   }], 
    38             ['worker',        { Default => undef, Scope => 'blog'   }], 
    39         ]), 
    40         callbacks      => { 
    41             'BuildFileFilter' => { 
    42                 priority => 10, 
    43                 code     => \&build_file_filter, 
    44             }, 
    45         } 
    46     }); 
    4764    MT->add_plugin($plugin); 
    4865} 
    4966 
     67sub instance { 
     68    $plugin; 
     69} 
     70 
     71sub enable { 
     72    $ENABLED = 1; 
     73} 
     74 
     75sub disable { 
     76    $ENABLED = 0; 
     77} 
     78 
     79sub enabled { 
     80    $ENABLED; 
     81} 
     82 
     83sub init_request { 
     84    my $plugin = shift; 
     85    my ($app) = @_; 
     86 
     87    $plugin->enable; 
     88    if ($app->isa('MT::App::CMS')) { 
     89        my $mode = $app->mode; 
     90        $plugin->disable if $mode =~ m/^rebuild/; 
     91    } 
     92 
     93    $plugin->SUPER::init_request(@_); 
     94} 
     95 
     96# The RebuildQueue can synchronize built pages, but in order to handle 
     97# files uploaded through the interface, we need to manage our own 
     98# FileInfo records. Upon synchronization of these, they can be removed. 
     99sub cms_upload_file { 
     100    my ($cb, %args) = @_; 
     101 
     102    my $url = $args{Url}; 
     103    my $file = $args{File}; 
     104    return unless -f $file; 
     105 
     106    return unless $plugin->sync_support; 
     107 
     108    my $blog = $args{Blog}; 
     109    my $blog_id = $blog->id; 
     110    return unless $plugin->blog_enabled($blog_id); 
     111 
     112    require MT::FileInfo; 
     113    my $base_url = $url; 
     114    $base_url =~ s!^https?://[^/]+!!; 
     115    my $fi = MT::FileInfo->load({ blog_id => $blog_id, url => $base_url }); 
     116    if (!$fi) { 
     117        $fi = new MT::FileInfo; 
     118        $fi->blog_id($blog_id); 
     119        $fi->url($base_url); 
     120        $fi->file_path($file); 
     121    } else { 
     122        $fi->file_path($file); 
     123    } 
     124    $fi->save; 
     125 
     126    require RebuildQueue::File; 
     127    my $rqf = RebuildQueue::File->load($fi->id); 
     128    if (!$rqf) { 
     129        $rqf = new RebuildQueue::File; 
     130        $rqf->id($fi->id); 
     131    } 
     132    $rqf->worker($plugin->blog_worker($blog_id)); 
     133    $rqf->sync_me(1); 
     134    $rqf->save; 
     135} 
     136 
     137sub fileinfo_post_remove { 
     138    my ($cb, $fi) = @_; 
     139    require RebuildQueue::File; 
     140    if (my $rqf = RebuildQueue::File->load($fi->id)) { 
     141        $rqf->remove; 
     142    } 
     143} 
     144 
     145sub fileinfo_post_remove_all { 
     146    my ($cb, $fi) = @_; 
     147    require RebuildQueue::File; 
     148    RebuildQueue::File->remove_all; 
     149} 
     150 
    50151sub system_config_template { 
     152    my $workers = $plugin->get_config_value('workers', 'system'); 
     153 
     154    my $max = 10; 
     155    $max = $workers + 10 if $workers >= $max; 
     156 
     157    my $worker_html = ''; 
     158    for (my $i = 1; $i <= $max; $i++) { 
     159        $worker_html .= qq{ 
     160    <option value="$i" <TMPL_IF NAME=WORKERS_$i>selected="selected"</TMPL_IF>>$i</option>}; 
     161    } 
     162 
    51163    return <<HTML; 
    52164<div class="setting"> 
    53165<div class="label"><MT_TRANS phrase="Number of Workers:"></div> 
    54166<div class="field"><ul><li><select name="workers"> 
    55     <option value="1" <TMPL_IF NAME=WORKERS_1>selected="selected"</TMPL_IF>>1</option> 
    56     <option value="2" <TMPL_IF NAME=WORKERS_2>selected="selected"</TMPL_IF>>2</option> 
    57     <option value="3" <TMPL_IF NAME=WORKERS_3>selected="selected"</TMPL_IF>>3</option> 
    58     <option value="4" <TMPL_IF NAME=WORKERS_4>selected="selected"</TMPL_IF>>4</option> 
    59     <option value="5" <TMPL_IF NAME=WORKERS_5>selected="selected"</TMPL_IF>>5</option> 
    60     <option value="6" <TMPL_IF NAME=WORKERS_6>selected="selected"</TMPL_IF>>6</option> 
    61     <option value="7" <TMPL_IF NAME=WORKERS_7>selected="selected"</TMPL_IF>>7</option> 
    62     <option value="8" <TMPL_IF NAME=WORKERS_8>selected="selected"</TMPL_IF>>8</option> 
    63     <option value="9" <TMPL_IF NAME=WORKERS_9>selected="selected"</TMPL_IF>>9</option> 
    64     <option value="10" <TMPL_IF NAME=WORKERS_10>selected="selected"</TMPL_IF>>10</option> 
     167$worker_html 
    65168</select></li></ul> 
     169</div> 
     170</div> 
     171 
     172<div class="setting"> 
     173<div class="label"><MT_TRANS phrase="Synchronize:"></div> 
     174<div class="field"><ul><input name="rebuildq_sync" type="checkbox" <TMPL_IF NAME=REBUILDQ_SYNC_1>checked="checked"</TMPL_IF> value="1" /> 
     175<MT_TRANS phrase="Check to support synchronization of queued items with other servers."> 
     176</li></ul> 
    66177</div> 
    67178</div> 
     
    121232} 
    122233 
     234sub sync_support { 
     235    $plugin->get_config_value('rebuildq_sync') ? 1 : 0; 
     236} 
     237 
     238sub blog_enabled { 
     239    my $plugin = shift; 
     240    my ($blog_id) = @_; 
     241    $plugin->get_config_value('rebuildq_mode', 'blog:'.$blog_id) ? 1 : 0; 
     242} 
     243 
     244sub blog_worker { 
     245    my $plugin = shift; 
     246    my ($blog_id) = @_; 
     247    my $workers = $plugin->get_config_value('workers') || 1; 
     248    my $worker = $plugin->get_config_value('workers', 'blog:'.$blog_id); 
     249    $worker = 1 if (defined $worker) && ($worker > $workers); 
     250    defined $worker ? $worker : int(rand($workers)) + 1; 
     251} 
     252 
     253# Adds an element to the rebuild queue when the plugin is enabled. 
    123254sub build_file_filter { 
    124255    my ($cb, %args) = @_; 
     256    return 1 unless $ENABLED; 
    125257 
    126258    my $fi = $args{FileInfo}; 
    127259    return 1 unless $fi; 
    128260 
    129     my $mode = $plugin->get_config_value('rebuildq_mode', 'blog:'.$fi->blog_id); 
    130     return 1 unless $mode; 
     261    return 1 unless $plugin->blog_enabled($fi->blog_id); 
    131262 
    132263    require RebuildQueue::File; 
     
    138269        $rqf->id($fi->id); 
    139270    } 
    140     my $workers = $plugin->get_config_value('workers') || 1; 
    141     my $worker = $plugin->get_config_value('workers', 'blog:'.$fi->blog_id); 
    142     $worker = 1 if (defined $worker) && ($worker > $workers); 
    143271 
    144272    $rqf->rebuild_me(1); 
    145273    $rqf->sync_me(0); 
    146     $rqf->worker(defined $worker ? $worker : int(rand($workers))+1); 
     274    $rqf->build_time(0); 
     275    $rqf->worker($plugin->blog_worker($fi->blog_id)); 
    147276 
    148277    my $at = $fi->archive_type || ''; 
     
    178307} 
    179308 
     309# Something was built while the queue itself was disabled; make sure 
     310# the item is marked for syncing. 
     311sub build_file { 
     312    my ($cb, %args) = @_; 
     313 
     314    return unless $plugin->sync_support; 
     315 
     316    my $fi = $args{FileInfo}; 
     317    my $blog = $args{Blog}; 
     318    if (!$fi) { 
     319        # FileInfo may still be there due to a bug in MT::WeblogPublisher 
     320        # passing an undef FileInfo element... 
     321        require MT::FileInfo; 
     322        $fi = MT::FileInfo->load({ blog_id => $blog->id, file_path => $args{File} }); 
     323    } 
     324    return unless $fi; 
     325    return unless $plugin->blog_enabled($blog->id); 
     326 
     327    require RebuildQueue::File; 
     328    my $rqf = RebuildQueue::File->load($fi->id); 
     329    if (!$rqf) { 
     330        $rqf = new RebuildQueue::File; 
     331        $rqf->id($fi->id); 
     332        $rqf->build_time(time); 
     333        $rqf->worker($plugin->blog_worker($blog->id)); 
     334        $rqf->sync_me(1); 
     335        $rqf->save; 
     336    } else { 
     337        if (!$rqf->rebuild_me) { 
     338            if (!$rqf->sync_me) { 
     339                $rqf->sync_me(1); 
     340                $rqf->build_time(time); 
     341                $rqf->worker($plugin->blog_worker($blog->id)); 
     342                $rqf->save; 
     343            } 
     344        } 
     345    } 
     346} 
     347 
    1803481; 
     349 
     350__END__ 
     351 
     352=head1 NAME 
     353 
     354RebuildQueue::Plugin - Movable Type plugin for distributed rebuilding. 
     355 
     356=head1 SYNOPSIS 
     357 
     358=head1 TWEAKING 
     359 
     360You may wish to further tune the rebuild queue items. To do so, you 
     361should hook into the RebuildQueue::File::pre_save MT callback. This 
     362gives you a chance to modify the priority and build_time properties 
     363of a RebuildQueue::File record before it is saved. Here's an example: 
     364 
     365Within a separate plugin, register for the callback: 
     366 
     367    MT->add_callback('RebuildQueue::File', 5, undef, \&rqf_fix); 
     368 
     369Then, define your callback routine: 
     370 
     371    sub rqf_fix { 
     372        my ($cb, $obj, $orig) = @_; 
     373        my $fi = $obj->fileinfo; 
     374 
     375        # only tweak when being saved to the rebuild queue 
     376        return unless $obj->rebuild_me; 
     377 
     378        # Sidebar elements can be built last. 
     379        if ($fi->file_path =~ m!/sidebar/!) { 
     380            $obj->priority(1000); # really low priority 
     381        } 
     382 
     383        # Forces pages built for nagios to run on a particular 
     384        # worker that also has access to nagios local data. 
     385        if ($fi->file_path =~ m!/nagios/!) { 
     386            $obj->worker(3); 
     387        } 
     388    } 
     389 
     390As you can see, it's possible to really fine-tune your rebuild 
     391queue. 
     392 
     393=head1 AUTHORS 
     394 
     395Brad Choate and Jay Allen 
     396 
     397=head1 COPYRIGHT 
     398 
     399Copyright (c) 2006, Brad Choate and Jay Allen. This is free software. 
     400It may be distributed and modified under the same terms as Perl itself. 
     401 
     402=head1 AVAILABILITY 
     403 
     404http://code.sixapart.com/ 
     405 
     406=cut