Show Contents Previous Page Next Page Chapter 5 - Maintaining State / Storing State at the Server Side Because Apache server processes are persistent across multiple accesses, you can store small amounts of state information in main memory. When the user first runs your application, it generates a random unique session identifier (session ID) and stores the state information in a data structure, for instance, a hash table keyed by the session ID. The application then sends the session ID back to the user in the form of a cookie, a hidden field, or a component of the URI. When the same user connects again, your application recovers the session ID and retrieves the state information from its data structure. Sounds simple, but there are some catches. On Win32 systems this scheme works flawlessly because there is only one server process and one single-threaded Perl interpreter. However, on Unix systems there are multiple Apache processes running simultaneously, each with its own memory space. When a user fetches a page, there's no guarantee that he will connect to the same server process as before. What's more, server processes do die from time to time when they reach the limit specified by Apache's MaxRequestsPerChild directive.
If you are using As a bonus, the shared data structures persist even when the processes using it go away, so the state information will survive even a complete server shutdown and restart (but not a system reboot). The downside is that working with the shared data structures is not entirely transparent. You have to lock the tied variables prior to updating them and use them in a way that doesn't cause excessive consumption of system resources. IPC::Shareable is available on CPAN. It requires Raphael Manfredi's Storable module as well. Here's the idiom for placing a hash in shared memory: tie %H, 'IPC::Shareable', 'Test', {create => 1, mode => 0666}; The first argument gives the name of the variable to tie, in this case
If successful, tie() will tie Shared hashes work a lot like ordinary hashes. You can store scalar variables or complex data structures into its keys. Any of these code fragments is legal: $H{'fee'} = 'I smell the blood'; $H{'fie'} = ['of', 'an', 'englishman']; $H{'foe'} = {'and' => 'it', 'makes' => 'me', 'very' => 'hungry'}; $H{'fum'}{'later'} = 'Do you have any after dinner mints?'; You can also store blessed objects into shared variables but not into filehandles or globs. It's important to realize what is and what is not tied when you use IPC::Shareable. In the first example we copy a simple scalar value into shared memory space. Any changes that we make to the value, such as a string substitution, are immediately visible to all processes that share the variable. In the second example, we construct an anonymous array and copy it into the shared variable. Internally IPC::Shareable uses the Storable freeze() function to serialize the structure into a binary representation and then place it in shared memory. As a consequence, changing an individual array element will not propagate correctly to other processes: $H{'fie'}[2] = 'frenchman'; # this change will NOT propagate Instead, you must copy the array into ordinary memory, make the changes, and copy it back: my $temp = $H{'fie'}; $temp->[2] = 'frenchman'; $H{'fie'} = $temp; For similar reasons we must also use this workaround to change elements in the third example, where the value is an anonymous hash.
Oddly enough, the fourth example behaves differently. In this case, we assign a value to an "automatic" anonymous hash. The hash is automatic because before the assignment, the key $H{'fum'} = {}; $H{'fum'}{'later'} = 'Do you have any after dinner mints?'; Although this sounds like a neat feature, it can be a programming trap. Each tied hash that is created by this method occupies its own shared memory segment. If you use this feature too liberally, you'll end up exhausting your system's shared memory segments and subsequent attempts to tie variables will fail.
Another trap involves updating shared variables. Many update operations aren't atomic, even simple ones like tied(%H)->shlock; $H{'englishmen eaten'}++; tied(%H)->shunlock; Example 5-5 shows the code for Hangman5.
The top of the file now loads the IPC::Shareable module and defines
a shared global named use IPC::Shareable (); use constant SIGNATURE => 'HANG'; use constant COOKIE_NAME => 'SessionID5'; use constant MAX_SESSIONS => 100; use vars qw(%SESSIONS);
# bind session structure to shared memory bind_sessions() unless defined(%SESSIONS) && tied(%SESSIONS); # fetch or generate the session id my $session_id = get_session_id();
The first step in the revised script is to call a new subroutine named bind_sessions() to tie the # get rid of old sessions to avoid consuming resources expire_old_sessions($session_id);
Next comes a call to expire_old_sessions() with the current session ID as the argument. Because we're keeping the session information in a limited resource, we must be careful to remove old sessions when they're no longer in use. We accomplish this by maintaining a rolling list of active sessions. The current session is moved to the top of the list while older sessions drift downward to the bottom. When the list exceeds a preset limit of simultaneous sessions ( The remainder of the body of the script should look very familiar. It's modified only very slightly from the examples we've seen before: # retrieve the state my $state = get_state($session_id) unless param('clear'); # reinitialize if we need to $state = initialize($state) if !$state or param('restart'); # process the current guess, if any my($message, $status) = process_guess(param('guess') || '', $state); # save the modified state save_state($state, $session_id);
The get_state() function now takes the session ID as its argument. It retrieves the state from the # start the page print header(-Cookie => => cookie(-name => COOKIE_NAME, -value => $session_id, -expires => '+1h')); The last task is to associate the session ID with the user. We do this by handing the remote browser a cookie containing the ID. Unlike the previous example, this cookie is set to expire after an hour of idle time. We expect the sessions to turn over rapidly, so it doesn't make sense to save the session ID for any longer than that. Although this might seem similar to the previous cookie examples, the big difference is that the cookie doesn't hold any state information itself. It's just a tag for the information stored at the server side. Let's now turn to the new subroutines: # Bind the session variables to shared memory using IPC::Shareable sub bind_sessions { die "Couldn't bind shared memory" unless tie %SESSIONS, 'IPC::Shareable', SIGNATURE, {create => 1, mode => 0644}; }
The bind_sessions() function calls tie() to bind The get_session_id() method is responsible for choosing a unique ID for new sessions, or recovering the old ID from ongoing sessions: sub get_session_id { my $id = cookie(COOKIE_NAME); return $id if defined($id) and exists $SESSIONS{$id}; # Otherwise we have to generate an id. # Use the random number generator to find an unused key. tied(%SESSIONS)->shlock; do { $id = sprintf("%8d", 1E8*rand()); } until !exists($SESSIONS{$id}); # must avoid assigning an empty hash to IPC::Shareable $SESSIONS{$id} = {WORD => ''}; tied(%SESSIONS)->shunlock; $id; } get_session_id() first attempts to recover a previously assigned
session ID from the browser cookie. If the cookie does exist, and the session
ID is still valid (it's a valid key for The expire_old_sessions() subroutine is responsible for garbage-collecting old session information that is no longer in use: sub expire_old_sessions { my $id = shift; tied(%SESSIONS)->shlock; my @sessions = grep($id ne $_, @{$SESSIONS{'QUEUE'}}); unshift @sessions, $id; if (@sessions > MAX_SESSIONS) { my $to_delete = pop @sessions; delete $SESSIONS{$to_delete}; } $SESSIONS{'QUEUE'} = \@sessions; tied(%SESSIONS)->shunlock; }
This subroutine works by maintaining a sorted list of sessions in an anonymous array located at the special key sub get_state { my $id = shift; return undef unless $SESSIONS{$id} and $SESSIONS{$id}{'WORD'}; $SESSIONS{$id}; } sub save_state { my($state, $id) = @_; $SESSIONS{$id} = $state; } get_state() and save_state() are trivial in this implementation. get_state() looks up the state information in Example 5-5. The Hangman Game with Server-Side State in Shared Memory # file: hangman5.cgi # hangman game using IPC::Shareable and cookies use IO::File (); use CGI qw(:standard); use CGI::Cookie (); use IPC::Shareable (); use strict; use constant WORDS => '/usr/games/lib/hangman-words'; use constant ICONS => '/icons/hangman'; use constant TRIES => 6; use constant SIGNATURE => 'HANG'; use constant COOKIE_NAME => 'SessionID5'; use constant MAX_SESSIONS => 100; use vars qw(%SESSIONS); # bind session structure to shared memory bind_sessions() unless defined(%SESSIONS) && tied(%SESSIONS); # fetch or generate the session id my $session_id = get_session_id(); # get rid of old sessions to avoid consuming resources expire_old_sessions($session_id); # retrieve the state my $state = get_state($session_id) unless param('clear'); # reinitialize if we need to $state = initialize($state) if !$state or param('restart'); # process the current guess, if any my($message, $status) = process_guess(param('guess') || '', $state); # save the modified state save_state($state, $session_id); # start the page print header(-Cookie => cookie(-name => COOKIE_NAME, -value => $session_id, -expires => '+5d')), . . . everything in the middle remains the same . . .
# Bind the session variables to shared memory using IPC::Shareable sub bind_sessions { die "Couldn't bind shared memory" unless tie %SESSIONS, 'IPC::Shareable', SIGNATURE, {create => 1, mode => 0666}; } # Fetch or generate the session ID. # It's simply a key into the %SESSIONS variable sub get_session_id { my $id = cookie(COOKIE_NAME); return $id if defined($id) and exists $SESSIONS{$id}; # Otherwise we have to generate an id. # Use the random number generator to find an unused key. tied(%SESSIONS)->shlock; do { $id = sprintf("%8d", 1E8*rand()); } until !exists($SESSIONS{$id}); # must avoid assigning an empty hash to IPC::Shareable's tied arrays $SESSIONS{$id} = {WORD => ''}; tied(%SESSIONS)->shunlock; $id; } # bring the current session to the front and # get rid of any that haven't been used recently sub expire_old_sessions { my $id = shift; tied(%SESSIONS)->shlock; my @sessions = grep($id ne $_, @{$SESSIONS{'QUEUE'}}); unshift @sessions, $id; if (@sessions > MAX_SESSIONS) { my $to_delete = pop @sessions; delete $SESSIONS{$to_delete}; } $SESSIONS{'QUEUE'} = [@sessions]; tied(%SESSIONS)->shunlock; } # Retrieve an existing state sub get_state { my $id = shift; my $s = $SESSIONS{$id}; return undef unless $s and $s->{WORD}; return $SESSIONS{$id}; } # Save the current state sub save_state { my($state, $id) = @_; $SESSIONS{$id} = $state; } The main problem with this technique is that the amount of state information that you can store in shared memory is very limited, making it unsuitable for high-volume or high-reliability applications. A better server-side solution involves using database management systems, which we turn to in the next section. Footnotes 5 The octal modes used in IPC::Shareable are similar to file modes and have the same effect on other processes' ability to access the data. Do not confuse them with umask, which has no effect on shared memory. 6 Using rand() is not the best way to create unique IDs, because it makes them easy to guess. However, it's simple and fast. The section on DBI databases presents a way to generate hard-to-guess IDs using the MD5 digest function. Show Contents Previous Page Next PageCopyright © 1999 by O'Reilly & Associates, Inc. |
HIVE: All information for read only. Please respect copyright! |