Maintaining state
One of the biggest drawbacks of the earlier example is that it fetches the full set of results at one time, which is impractical for large result sets. It is more practical to display a full set of results across multiple pages and allow the user to move forward or backward through the pages.
This is simple in a conventional application where a connection to the separate server is maintained until the user terminates the application. In a web implementation however, this seemingly simple requirement involves a considerably higher level of complexity due to the stateless nature of web pages. One such complexity is that each time a new page of results is displayed, the initial search for the records must be re-executed. This is inconvenient for the web programmer and potentially slow for the user.
The IMu server provides a solution to this. When a handler object is created, a corresponding object is created on the server to service the handler's request: this server-side object is allocated a unique identifier by the IMu server. When making a request for more information, the unique identifier can be used to connect a new handler to the same server-side object, with its state intact.
The following example illustrates the connection of a second, independently
created IMu::Module
object to the same server-side object:
# Create a module object as usual
my $first = IMu::Module->new('eparties', $session); # Run a search - this will create a server-side object my $keys = [1, 2, 3, 4, 5, 42]; $first->findKeys($keys); # Get a set of results my $result1 = $first->fetch('start', 0, 2, 'SummaryData'); # Create a second module object my $second = IMu::Module->new('eparties', $session); # Attach it to the same server-side object as the first module. # This is the key step. $second->{'id'} = $first->{'id'}; # Get a second set of results from the same search my $result2 = $second->fetch('current', 1, 2, 'SummaryData');
Although two completely separate IMu::Module
objects have been created, they
are each connected to the same server-side object by virtue of having the same
id
property. This means that the second fetch
call will access the same
result set as the first fetch
. Notice that a flag of current
has been
passed to the second call. The current state is maintained on the server-side
object, so in this case the second call to Fetch
will return the third and
fourth records in the result set.
While this example illustrates the use of the id
property, it is not
particularly realistic as it is unlikely that two distinct objects which refer
to the same server-side object would be required in the same piece of code. The
need to re-connect to the same server-side object when generating another page
of results is far more likely. This situation involves creating a server-side
IMu::Module
object (to search the module and deliver the first set of
results) in one request and then re-connecting to the same server-side object
(to fetch a second set of results) in a second request. As before, this is
achieved by assigning the same identifier to the id
property of the object
in the second page, but two other things need to be considered.
By default the IMu server destroys all server-side objects when a session
finishes. This means that unless the server is explicitly instructed not to do
so, the server-side object may be destroyed when the connection from the first
page is closed. Telling the server to maintain the server-side object only
requires that the destroy
property on the object is set to false
before
calling any of its methods. In the example above, the server would be
instructed not to destroy the object as follows:
my $module = IMu::Module->new('eparties', $session); $module->setDestroy(0);my $keys = [1, 2, 3, 4, 5, 42]; $module->findKeys($keys);
The second point is quite subtle. When a connection is established to a server, it is necessary to specify the port to connect to. Depending on how the server has been configured, there may be more than one server process listening for connections on this port. Your program has no control over which of these processes will actually accept the connection and handle requests. Normally this makes no difference, but when trying to maintain state by re-connecting to a pre-existing server-side object, it is a problem.
For example, suppose there are three separate server processes listening for connections. When the first request is executed it connects, effectively at random, to the first process. This process responds to the request, creates a server-side object, searches the Parties module for the terms provided and returns the first set of results. The server is told not to destroy the object and passes the server-side identifier to another page which fetches the next set of results from the same search.
The problem comes when the next page connects to the server again. When the connection is established any one of the three server processes may accept the connection. However, only the first process is maintaining the relevant server-side object. If the second or third process accepts the connection, the object will not be found.
The solution to this problem is relatively straightforward. Before the first
request closes the connection to its server, it must notify the server that
subsequent requests need to connect explicitly to that process. This is
achieved by setting the IMu::Session
object's suspend
property to true
prior to submitting any request to the server:
my $session = IMu::Session->new('server.com', 12345);my $module = IMu::Module->new('eparties', $session); $session->setSuspend(1); $module->findKeys($keys);
The server handles a request to suspend a connection by starting to listen for
connections on a second port. Unlike the primary port, this port is guaranteed
to be used only by that particular server process. This means that a subsequent
page can reconnect to a server on this second port and be guaranteed of
connecting to the same server process. This in turn means that any saved
server-side object will be accessible via its identifier. After the request has
returned (in this example it was a call to findKeys
), the IMu::Session
object's port
property holds the port number to reconnect to:
$session->setSuspend(1); $module->findKeys($keys); $reconnect = $session->{'port'};
Example
To illustrate we'll modify the very simple results page of the earlier
section to display the list of matching names in
blocks of five records per page. We'll provide simple Next and Prev links
to allow the user to move through the results, and we will use some more
GET
parameters to pass the port we want to reconnect to, the identifier of
the server-side object and the rownum
of the first record to be displayed.
First build the search page, which is a plain HTML form:
<head> <title>Party Search</title> </head> <body> <form action="/cgi-bin/example.pl"> <p>Enter a last name to search for (e.g. S*):</p> <input type="text" name="name"/> <input type="submit" value="Search"/> </form> </body>
Next build the results page (source code), which runs the search and displays the results. The steps to build the search page are outlined in detail below.
- Create a
IMu::Session
object. Then theport
property is set to a standard value unless a port parameter has been passed in the URL. The core Perl moduleCGI
is used to handle getting parameters from the URL and outputting HTML elements:#
# Create new session object.
#
my $session = IMu::Session->new();
$session->setHost('imu.mel.kesoftware.com');
#
# Work out what port to connect to
#
my $port = 40136;
if (defined($cgi->param('port')) && $cgi->param('port') ne '')
{
$port = $cgi->param('port')
}
$session->setPort($port);
- Connect to the server and immediately set the
suspend
property to1
(true) to tell the server that we may want to connect again:#
# Establish connection and tell the server we may want to re-connect
#
$session->connect();
$session->setSuspend(1);
This ensures the server listens on a new, unique port.
- Create the client-side
IMu::Module
object and set itsdestroy
property to 0 (false):#
# Create module object and tell the server not to destroy it.
#
my $module = IMu::Module->new('eparties', $session);
$module->setDestroy(0);
This ensures that the server will not destroy the corresponding server-side object when the session ends.
- If the URL included a
name
parameter, we need to do a new search. Alternatively, if it included an id parameter, we need to connect to an existing server-side object:#
# If name is supplied, do new search.
#
my $name = $cgi->param('name');
my $id = $cgi->param('id');
if (defined($name) && $name ne '')
{
my $terms = IMu::Terms->new();
$terms->add('NamLast', $name);
$module->findTerms($terms);
}
#
# Otherwise, if id is supplied reattach to existing server-side object
#
elsif (defined($id) && $id ne '')
{
$module->{'id'} = $id;
}
#
# Otherwise, we can't process
#
else
{
print($cgi->header('text/html','400 Bad Request'));
die("missing 'name' or 'id' parameter\n");
}
- Build a list of columns to fetch:
my $columns =
[
'NamFirst',
'NamLast'
];
- If the URL included a
rownum
parameter, fetch records starting from there. Otherwise start from record number1
:#
# Work out which block of records to fetch
#
my $rownum = 1;
if (defined($cgi->param('rownum')))
{
$rownum = $cgi->param('rownum');
}
- Build the main page:
#
# Fetch next five records
#
my $results = $module->fetch('start', $rownum - 1, 5, $columns);
my $hits = $results->{'hits'};
#
# Build the results page
#
print($cgi->header());
print($cgi->start_html(-title => 'IMu Perl API - Maintaining State'));
print($cgi->p("Number of matches: $hits"));
#
# Display each match in a separate row in a table
#
print($cgi->start_table());
foreach my $row (@{$results->{'rows'}})
{
my $rowNum = $row->{'rownum'};
my $firstName = $row->{'NamFirst'};
my $lastName = $row->{'NamLast'};
print($cgi->start_Tr());
print($cgi->start_td(), "$rowNum.", $cgi->end_td());
print($cgi->start_td(), "$lastName, $firstName", $cgi->end_td());
print($cgi->end_Tr());
}
print($cgi->end_table(), "\n");
- Finally, add the Prev and Next links to allow the user to page backwards
and forwards through the results. This is the most complicated part! First,
to ensure that a connection is made to the same server and server-side
object, add the appropriate
port
andid
parameters to the link URL:#
# Add the Prev and Next links
#
my $url = $cgi->url();
$url .= '?port=' . $session->{'port'};
$url .= '&id=' . $module->{'id'};
- If the first record is not showing add a Prev link to allow the user to go
back one page in the result set. Similarly, if the last record is not
showing add a Next link to allow the user to go forward one page:
my $first = $results->{'rows'}->[0];
if ($first->{'rownum'} > 1)
{
my $prev = $first->{'rownum'} - 5;
if ($prev < 1)
{
$prev = 1;
}
$prev = $url . '&rownum=' . $prev;
print($cgi->a({href => $prev}, 'Prev'), "\n");
}
my $count = @{$results->{'rows'}};
my $last = $results->{'rows'}->[$count - 1];
if ($last->{'rownum'} < $results->{'hits'})
{
my $next = $last->{'rownum'} + 1;
$next = $url . '&rownum=' . $next;
print($cgi->a({href => $next}, 'Next'));
}
print($cgi->end_html());
exit(0);