Article Index
Nathan M. Andelin   September 2017

I/O Servers

Friends and colleagues approached me about a month or so ago with a request to integrate a service program of their design into a basic database inquiry and maintenance application of mine. They dubbed their service program an "I/O server".

These folks are engaged in an education-marketing campaign, fervently urging shops to replace RPG I/O op codes that have traditionally been embedded in business applications, with calls to I/O servers instead, under the auspices of "modernizing" database I/O.

The idea is to create an I/O server for every physical and logical file in your database, and make calls to I/O server procedures instead of embedding RPG op codes within applications. A tool may be licensed that automates the generation of an I/O servers from file meta-data, based on RPG templates.

The procedural interface for an I/O server consists of three (3) parameters. The first two parameters are required. The third parameter is optional based on whether the requested I/O calls for a record key.

  1. Action Code (e.g. I=Insert, U=Update, D=Delete, R=Read, S=Setll, G=Setgt, etc.).
  2. A pointer to a record buffer that is shared between the application and the I/O server.
  3. A data structure that contains the key(s) to a file.

Following is a sample source-code module for this type of I/O server:

     h nomain aut(*use) option(*nodebugio) debug bnddir('AMWSRV')

     fcusmstf   uf a e           k disk    usropn commit rename(cusmstfr:rec)

     d cusmstfp        s               *

     d cusmstfk      e ds                  extname(cusmstf:*key) qualified
     d cusmstfr      e ds                  extname(cusmstf) based(cusmstfp)

     d cusmstio        pr              n
     d  action                        5    const
     d  pointer                        *   const
     d  keys                               const options(*nopass) like(cusmstfk)

      * move *diag & re-send *escape messages (percolate)

     d err03           pr
     d  mt                            1    const options(*nopass)

      * send *escape message

     d err11           pr
     d  mi                            7    const
     d  mf                           10    const options(*omit:*nopass)
     d  md                          128    const options(*omit:*nopass)
     d  mt                            1    const options(*nopass)

     d put             pr              n
     d upd             pr              n
     d dlt             pr              n
     d sets            pr              n
     d setg            pr              n
     d get             pr              n
     d getnxt          pr              n
     d getprv          pr              n

     d savePtr         s               *   inz(%addr(saverec))
     d saverec         ds                  likerec(rec)
     d saveKey         ds                  likerec(rec:*key)
     d saveKey#        s              5i 0

     d action          ds             5
     d  a1                            1
     d  a2                            1

     d keys            s              5i 0

      // ----------------------------------------------------------------
      //  perform I/O on the customer master file
      // ----------------------------------------------------------------

     p cusmstio        b                   export
     d cusmstio        pi              n
     d  A                             5    const
     d  P                              *   const
     d  K                                  const options(*nopass) like(cusmstfk)

      /free

       monitor;

        if not %open(cusmstf);
         open cusmstf;
        endif;

        action   = A;
        cusmstfp = P;

        select;
         when %parms = 2 and a1 = 'D';
          eval-corr cusmstfk = cusmstfr;
         when %parms = 3;
          cusmstfk = K;
        endsl;

        select;
         when a1 = 'I';   // Insert
          return put();
         when a1  = 'U';  // update
          return upd();
         when a1  = 'D';  // Delete
          return dlt();
         when a1  = 'R';  // Read
          return get();
         when a1  = 'S';  // Set Limits (SETLL)
          return sets();
         when a1  = 'G';  // Set Limits (setgT)
          return setg();
         when a1 = 'X';   // Close
          close cusmstf;
          *inlr = *on;
          return *on;
         other;
          err11('IOS9999':'AMWMSGF':'cusmstio  '+action);
        endsl;

       on-error;
        err03();
       endmon;

      /end-free

     p cusmstio        e

      // ----------------------------------------------------------------
      //  write a record
      // ----------------------------------------------------------------

     p put             b
     d put             pi              n

      /free

       monitor;

        setll *hival cusmstf;
        readp(n) cusmstf saverec;

        if %eof;
         custno = 1;
        else;
         custno = saverec.custno + 1;
        endif;

        strdte = %date();

        write rec;

        return *on;

       on-error;
        err03();
       endmon;

      /end-free

     p put             e

      // ----------------------------------------------------------------
      //  update a record
      // ----------------------------------------------------------------

     p upd             b
     d upd             pi              n

      /free

       monitor;

        eval-corr cusmstfk = cusmstfr;

        chain %kds(cusmstfk) cusmstf saverec;

        select;
         when %found;
          update rec;
          return *on;
         when a2 = 'I';
          return put();
         other;
          return *off;
        endsl;

       on-error;
        err03();
       endmon;

      /end-free

     p upd             e

      // ----------------------------------------------------------------
      //  delete a record
      // ----------------------------------------------------------------

     p dlt             b
     d dlt             pi              n

      /free

       monitor;

        delete %kds(cusmstfk) cusmstf;

        return *on;

       on-error;
        err03();
       endmon;

      /end-free

     p dlt             e

      // ----------------------------------------------------------------
      //  set lower limit / check existence
      // ----------------------------------------------------------------

     p sets            b
     d sets            pi              n

      /free

       monitor;

        setll %kds(cusmstfk) cusmstf;

        return %equal;

       on-error;
        err03();
       endmon;

      /end-free

     p sets            e

      // ----------------------------------------------------------------
      //  set grater than / check existence
      // ----------------------------------------------------------------

     p setg            b
     d setg            pi              n

      /free

       monitor;

        setgt %kds(cusmstfk) cusmstf;

        return %found;

       on-error;
        err03();
       endmon;

      /end-free

     p setg            e

      // ----------------------------------------------------------------
      //  retrieve a record
      // ----------------------------------------------------------------

     p get             b
     d get             pi              n

      /free

       monitor;

        select;
         when a2 = ' ';  // Read - All keys
          chain(n) %kds(cusmstfk) cusmstf;
          return %found;
         when a2 = 'N';  // Read Next
          return getnxt();
         when a2 = 'P';  // Read Previous
          return getprv();
         when a2 = 'F';  // Read Next record
          read(n) cusmstf;
         when a2 = 'B';  // Read Previous record
          readp(n) cusmstf;
        endsl;

        return %eof;

       on-error;
        err03();
       endmon;

      /end-free

     p get             e

      // ----------------------------------------------------------------
      //  read next equal - don't lock
      // ----------------------------------------------------------------

     p getnxt          b
     d getnxt          pi              n

      /free

       monitor;

        reade(n) %kds(cusmstfk) cusmstf;

        return %eof;

       on-error;
        err03();
       endmon;

      /end-free

     p getnxt          e

      // ----------------------------------------------------------------
      //  read previous equal - don't lock
      // ----------------------------------------------------------------

     p getprv          b
     d getprv          pi              n

      /free

       monitor;

        readpe(n) %kds(cusmstfk) cusmstf;

        return %eof;

       on-error;
        err03();
       endmon;

      /end-free

     p getprv          e

Is this type of I/O interface familiar to you?

My initial thought for this piece was to delineate the pros and cons of replacing RPG I/O op codes in applications with calls to I/O servers. However, as I studied the architecture implemented in this type of I/O server, I couldn't come up with any pros.

Virtually every point I could think of, favored the embedding of RPG I/O op codes in applications, as opposed to invoking procedures in I/O servers from applications.

After reviewing the I/O-server source code in detail, one can see that the execution path entails a number of steps before an actual I/O op code is invoked:

  • Two (2) procedure calls (the entry-point procedure, plus one pertaining to requested actions).
  • Two (2) monitor, on-error blocks (embedded in procedures).
  • Two to three (2-3) select blocks (based on requested actions)

The I/O server interface is designed to percolate I/O related *diagnostic messages, and to re-send *escape messages up one level in the call-stack, as opposed to say something like transforming them into a more friendly format.

When opening a file in an I/O server, there is no option for selecting an open mode (e.g. input, update, output, overwrite, block buffering, etc.). Rather the file is opened in a mode that provides for all I/O operations and record-lock options.

The commit keyword on the "f" spec designates the use of commitment control (applications and I/O servers must run under commitment control).

What sort of impact might an elongated execution path, structure, and commitment control have on I/O performance? The following benchmarks show reading the CUSMSTF file front to back, first using the I/O server, then next by just embedding the "f" spec and read op codes in the program.

Read CUSMSTF file using I/O Server:

    h bnddir('AMWSRV')

     d cusmstfp        s               *   inz(%addr(cusmstfr))
     d cusmstfr      e ds                    extname(cusmstf)

     d ok              s               n   inz(*off)

     d cusmstio        pr              n
     d  Action                        5    const
     d  Pointer                        *   const
     d  Keys                               const options(*nopass) like(cusmstfk)

      /free

       dow ok = *off;
        ok = cusmstio('RF' : cusmstfp);
       enddo;

       *inlr = *on;
       return;

      /end-free

Read CUSMSTF file using the read op code:

     fcusmstf   if   e           k disk

      /free

       read cusmstfr;

       dow not %eof();
        read cusmstfr;
       enddo;

       *inlr = *on;
       return;

      /end-free

Given 600,000 rows in the CUSMSTF file, the call to the CUSMSTIO() procedure in a loop consumes 5.6 times more CPU than invoking the READ op code within a loop and checking for %eof().

Questioning the poor performance of the CUSMSTIO() procedure, one concerned reader asked whether the I/O server was running in a *new activation group, or whether the I/O involved opening and closing the file repeatedly?

The answer is NO on both counts. CUSMSTIO() runs in the *caller activation group, and the CUSMSTF file is opened only once.

The lag in READ performance in this case is mostly due to the I/O server being unable to take advantage of block mode processing. Other benchmarks suggest that the I/O server code structure adds 35-40 percent of unnecessary overhead to I/O op codes.

Given the lag in performance when using an I/O server, one might ask whether the performance might be offset by an improvement in application architecture. The answer is NO! This type of I/O server is problematic throughout.

A number of common use-cases call for multiple instances of the same file to be opened within the same Job (occasionally multiple instances of the same file to be opened within the same program), and for each instance of the file to have its own open data path.

An I/O server provides a SHARED open data path for any and all programs that are running in the same activation group. That would require work-a rounds in a number of use cases.

Some database designs implement multi-member tables. Big data is a possible use case. However the I/O server interface has no provision for members (partitions in Db2 parlance).

I/O servers are not designed to open multiple concurrent instances of files from different libraries.

The I/O server referenced in this piece implements just a small number of record positioning, READ, LOCK, and KEY options. In order to implement a full compliment of such options, including partial key options would greatly increase the amount of code required, the number of internal procedures encapsulated, and the number of SELECT block options therein. Actually, using partial keys is not even an option with the given interface (although it may be required by some use cases).

Record positioning and reading may involve criteria such as:

  • First.
  • Last.
  • Next.
  • Prior.
  • By relative record number.
  • Equal to.
  • Less than.
  • Greater than.
  • Less than or equal to.
  • Greater than or equal to.
  • Next equal to.
  • Prior equal to.
  • Next unequal to.
  • Prior unequal to.
  • Beginning of file.
  • End of file.
  • Etc.

A full complement of Action options would be quite extensive; should arguably be implemented as named constants and kept in a /copy member. I/O server actions may be invoked from many programs.

Encapsulating a full compliment of I/O procedures in this manner entails a lot of repeated structure, including repeated boiler-plate code, which is an aberration of the Don't-Repeat-Yourself principle of application design.

Any change to the layout of a record format in an I/O server would require recompiling the I/O server, plus recompiling every other module that referenced the data buffer. Otherwise it is probable that invalid data would be inserted into records, and you wouldn't even get the benefit of a file-level-check error.

Error monitoring and message percolation up the call stack in an I/O server is redundant, in that the calling modules must also monitor for the same.

Setting up commitment control is typically very disruptive to legacy applications. It's arguably not needed for single-record updates as implemented in I/O servers, but rather designed to ensure the integrity of multi-update transactions. Existing applications would be forced to implement commitment boundaries, commits, and rollbacks (not trivial).

Application architects often promote the idea of encapsulating and "externalizing" data validation, referential integrity constraints, business rules, mapping between business objects and database objects, and other functionality that may require database I/O. That may be the well-meaning motivation for promoting this type of I/O server.

However, that is NOT what these I/O servers are designed to do. The interface in this I/O-server implementation is constrained to the three (3) parameters that I referenced previously. These I/O servers merely provide a procedure interface that wraps RPG I/O op codes, and monitors for errors that may be returned by Db2 for i, or errors that may be returned by trigger programs.

Although some business logic could be included in this type of I/O server, it would most likely be better suited if placed outside (perhaps in a trigger program, or in a calling program). An example of such might be the logic for automatically generating a customer number in the CUSMSTF file.

This type of I/O server fits the definition of an anti-pattern. It may look attractive on the surface. It may work in a narrow context. But it turns out badly when applied in a broader context. One reader suggested that this type of interface is regressive.

In a future article, I may address how to gain the benefits of externalizing I/O related logic without the overhead, constraints, redundancy, and other problems associated with this type of I/O server.