Second-Hand RPC Timeout Code
Considered Harmful

July 26, 1991

How many of you can spell "XDR"? Hands up! Hmm ... I thought so.

Art Buchwald once had a column in which he claimed that there was only one solitary fruitcake (the food kind, Steve!) in the whole world and, at Christmas time, everyone passed it around among themselves. A similar situation has occurred on TPOCC. I'm not sure where it originated, but a single piece of code for detecting the availability of more XDR input has made the rounds of TPOCC and TPOCC-based software; the TSTOL, display, events, and reports subsystems appear to be the only ones not infected. While reusing code is a laudable practice, it generally helps if the reused code works.

Typically, a program's main routine sits in a loop, waiting on a select(2) call for input from another task. When select() returns, indicating that a socket is active, the program calls a subroutine to read and process input from that socket. The subroutine reads and processes all the available input with a loop that looks something like the following:

    more_data = TRUE ;
    while (more_data) {    	/* Read and process an input record. */
        if (!xdr_type (&stream.svr_xdrs, &first_item_in_record)) {
            if (stream.svr_error.re_status == RPC_TIMEDOUT) {
                more_data = FALSE ;
                continue ;
            } else {
                ... error ...
        ... read and process the rest of the record ...
        if (!xdr_skiprecord (&stream.svr_xdrs) {
            ... error ...

Although this is a fine looking piece of code, it doesn't work. Or, perhaps I should say, it is not guaranteed to work all the time. You shouldn't depend on timeouts to determine if there is more data to read.

The Problem

When you call TPOCC's stream_svr() function to create an XDR record stream, stream_svr() sets the timeout value for the stream to zero. In other words, an XDR read on that connection will timeout immediately if the requested data is not ready and waiting to be read. (The timeout actually occurs in a select() call down in the innards of XDR.)

TSTOL and the event logger used to run with timeouts set to zero. TSTOL logs every line of input and, when running a lengthy procedure, occasionally gets ahead of the event logger. If the logger lags far enough behind TSTOL, the "pipeline" between the two processes fills up and TSTOL blocks in the middle of writing an event message to the logger; i.e., TSTOL is suspended after writing a partial message out to the network. Eventually, the event logger catches up and proceeds to read the outstanding message. When timeouts were set to zero, the event logger would read the partial event message sent by TSTOL and, before TSTOL had a chance to resume execution and send the remainder of the message, the event logger's read would timeout. Because of the read "error", the logger would close its connection to TSTOL.

Setting the timeout period on a stream to a large enough, non-zero value allows programs like the event logger to process through momentary delays in network transfers. If, however, the program is using timeouts to check for more input, the program will hang for the specified timeout period after every batch of input received. For example, suppose the event logger's timeout period was set to 10 seconds. After reading an event message, the logger would be suspended until 10 seconds elapsed or until another event message was received from the same event source, whichever occurred first.

The forward-looking TNIF programmers tried tailoring the timeout period to its function. Before reading the initial data item in a record, the timeout period for the stream is set to zero. If the XDR read for the first item times out, then there is no data to be read. If the initial XDR read doesn't time out, then the timeout period is increased to 5 seconds and the remainder of the record is read and processed.

Is the problem solved? Not quite. Suppose the initial data item in a record is a 4-byte message tag. In order to read that first data item, XDR must read at least 8 bytes: a 4-byte record fragment header and the 4-byte message tag. It is unlikely, but not impossible, for the split in a partial network transfer to occur in those first 8 bytes. If this happens, the XDR read will time out after reading part of the data and return RPC_TIMEDOUT to the calling program. The program will misinterpret the timeout error as an indication of no more data and return to its main select() loop. (When select() signals that the remainder of the XDR record has been received, I'm not sure if XDR can simply pick up where it left off or if it will now be out of sync with the sending process.)

The Solution

The correct way to determine if another XDR record is waiting to be read is simply to use the facilities already provided by the XDR system library. This was discussed in the Section 3 of the TPOCC Implementation Guide, although some examples of the timeout scheme appear in other sections; my apologies for not paying more attention to the latter. More detailed information about XDR can be found in Sun's Network Programming Guide, in HP's Programming and Protocols for NFS Services, and in the man(1) pages for XDR.

In general, a program reads and processes an XDR record as follows:

    xdr_type1 (...) ;		/* Read item 1 in the record. */
    xdr_type2 (...) ;		/* Read item 2 in the record. */
    xdr_typeN (...) ;		/* Read item N in the record. */
    xdrrec_skiprecord (...) ;	/* Position to the start of the next record. */

To check if another record is ready to be read, a program should call xdrrec_eof(3) before calling xdrrec_skiprecord(3). xdrrec_eof() positions to the end of the current record and returns an indication of whether or not another record follows the current one in XDR's input buffer. Note that xdrrec_eof() only checks XDR's input buffer; it does not do a select() on the network connection to see if data is waiting to be read from the socket.

The processing loop presented on page 1 can now be rewritten to correctly read and process the available data. First, when you create the XDR stream, you should set its timeout value to a large number. Doing so will keep your program from choking on partial record transfers. If you're using TPOCC's stream_svr() function, the timeout value is set as follows:

    #include "strm_svr.h"	/* Stream server definitions. */
    svr_stream  stream ;
    if (stream_svr (stream, connection, FALSE)) {
        ... error ...
    stream.svr_wait.tv_sec = long_time ;
    stream.svr_wait.tv_usec = 0 ;

Then, in the subroutine that reads and processes input from a socket, the loop should be recoded to call xdrrec_eof():

    do {			/* Read and process an input record. */
        if (!xdr_type (&stream.svr_xdrs, &first_item_in_record)) {
            ... error ...
        ... read and process the rest of the record ...
        more_data = !xdrrec_eof (&stream.svr_xdrs) ;
        if (!xdr_skiprecord (&stream.svr_xdrs) {
            ... error ...
    } while (more_data) ;

If your program is exchanging ASCII strings with another task, you might wish to use TPOCC's XNET utilities to read and write the XDR strings over the network. The XNET functions provide a high-level interface to XDR, thus relieving the programmer from the low-level details of XDR networking. The functions automatically configure the XDR stream for long timeouts, switch the XDR operation code back and forth for reads and writes, call xdrrec_eof() and xdrrec_skiprecord() for reads, and call xdrrec_endofrecord() for writes. Not coincidentally, the TSTOL, display, and reports subsystems use the XNET utilities and don't suffer from the timeout problems addressed by this memo.

Most processes have a network connection to the state manager through which they receive directives and return status messages. The following code fragment illustrates how the XNET utilities are used to establish a connection with the state manager and to read and process directives from the state manager:

    char  directive[256], status_message[256] ;
    int  more_data ;
    void  *handle ;

    xnet_call ("host", "mission_task_stmgr", &handle) ;
    for ( ; ; ) {
        ... call SELECT() to check for input ...
        if (FD_ISSET (xnet_socket (handle), &read_mask))
        do {		/* Read a directive from the state manager. */
            xnet_read_string (handle, buffer, sizeof buffer, &more_data) ;
            ... process the directive ...
			/* Return status to the state manager. */
            xnet_write_string (handle, status_message, 0) ;
        } while (more_data) ;

See Section 3.6 of the TPOCC Implementation Guide for more information about the XNET utilities.

Alex Measday  /  E-mail