A way to create web application using Rivet

Rivet, a Tcl module for the Apache Web server, allows great improvement in the performance of big Tcl Web applications. Web application instances that live inside Apache child processes are loaded once and run many times to generate requested Web documents. Each of these instances can have persistent database connections.

Sometimes there is a need to run several Rivet applications inside one Apache server. There raises a problem. By default, one single Tcl interpreter is created for every Apache child process. Different applications (or several instances of the same application) may interfere in this single Tcl interpreter. They can overwrite each other's procedures and global variables.

Another problem occurs in the process of creating and testing an application. Source code changes fast and needs to be reloaded into Apache. Rivet handles this for scripts that are executed directly. But big applications typically have many library scripts. This library is sourced into the interpreter upon the application's loading or later, when it is needed. There's no way for Rivet to know when and how to reload these library scripts.

The simplest way is to restart Apache every time you change source code. That is very unhandy.

To solve both problems above, we can use slave interpreters. The slave is created for each application when needed and destroyed when the sources are changed. The code of different applications doesn't interfere. Every application restarts automatically without restarting the entire Apache server.

The URI structure used is: http://site.domain/path/web.tcl/document?parameters

The path element distinguishes between applications inside the same Apache server.

web.tcl is a main script for the application. It does the following:

Every page of the application is generatated by some Tcl procedure. To identify valid document generation procedures, all their names have the _doc suffix.

There are some problems with implementing this approach. Rivet isn't a Tcl package. It can't be loaded into a slave interpreter. Hence Rivet commands aren't accessible from slaves. Also, the Rivet commands "load_env", "load_headers" and "load_cookies" load environment items into the array variables of the parent interpreter. These variables aren't accessible from slaves. To overcome this, we should create appropriate commands in the parent interpreter and command aliases in the slaves.

Code examples

In apache config there should be a directive to set script for apache child initialization for Rivet, like this:

RivetServerConf ChildInitScript "source /etc/apache/rivet-childinit.tcl"

In "rivet-childinit.tcl" we add proc's to access environment items:

# a way for slave to access environment
proc rivet_env_exists {name} {return [info exists ::env($name)]}
proc rivet_env_get {name} {return $::env($name)}
proc rivet_cookie_exists {name} {return [info exists ::headers($name)]}
proc rivet_cookie_get {name} {return $::headers($name)}

Here is working example of web.tcl code:

#
# sample Tcl Rivet application
# main executable
#

# directory with library scripts
set lib_dir .

# list of library script names
set lib_files {library.tcl}

# slave interpreter path
set slave_path [pwd]

# make list of library script paths and modification times
set mtime_list {}
foreach file $lib_files {
  set path [file join $lib_dir $file]
  lappend mtime_list $path [file mtime $path]
}

# do we need to destroy slave?
if {[interp exists $slave_path]} {
  # get saved list of library scripts loaded into slave
  catch {interp eval $slave_path set restart_list} restart_list

  # are library scripts changed?
  if {$mtime_list != $restart_list} {
    puts stderr "deleting interpeter '$slave_path'"
    if {[catch {interp delete $slave_path} res]} {
      puts stderr $res
    }
  }
}

# do we need to create new slave?
if {![interp exists $slave_path]} {
    puts stderr "creating interpeter '$slave_path'"
    set i [interp create $slave_path]

    # make Rivet commands aliases in slave
    foreach cmd {var upload include parse headers makeurl load_headers load_env
load_cookies cookie rivet_env_exists rivet_env_get} {
        $i alias $cmd $cmd
    }

    # allow to find same packages in slave as in master
    $i eval [list set auto_path $auto_path]

    # load needed packages into slave
    $i eval {
        package require base64
    }

    # load library scripts into slave
    foreach {path mtime} $mtime_list {
      $i eval source $path
    }

    # remember loaded library scripts
    $i eval [list set restart_list $mtime_list]
}

# run application
interp eval $slave_path run

And here is working example of application library, with two simplest pages, default "index" page and "hello" page:

#
# sample Tcl Rivet application
# library script
#

# process request
proc run {} {
  # load request environment into PARENT interpreter
  load_env

  # get document name from uri
  if {[rivet_env_exists PATH_INFO]} {
    set docName [lindex [file split [rivet_env_get PATH_INFO]] 1]
  } else {
    set docName index
  }

  # run procedure to print document
  $docName\_doc
}

# print index page
proc index_doc {} {
  puts "This is index page, variables are: [var all]."
}

# print sample "Hello!" page
proc hello_doc {} {
  puts "This is Hello page!"
}

Some other details

RivetWishWay (last edited 2009-09-20 23:33:54 by localhost)