eXtremeDB Error Handling in C

As explained in the Error Handling page, the C API runtime functions return three categories of return codes: Status, Error and Fatal Error. The actual values of these return codes are enumerated in mco.h. Status codes are return codes that are less than or equal to 50 and have #define names that are prefixed with MCO_S_. Error codes are return codes that are greater than 50 and have #define names that are prefixed with MCO_E_. (Please refer to the C API Return Codes for a complete list.)

Handling Status Codes

Status codes don’t indicate an error but merely the status after an operation has been performed. For example, most eXtremeDB functions, if successful, return MCO_S_OK, or if a search function finds no objects corresponding to the specified key value, the status code MCO_S_NOTFOUND is returned. A status code returned by a function does not affect the state of the transaction context within which the function was executed.

Handling Error Codes

Error codes, in contrast, indicate the runtime’s failure to successfully complete an operation. For example, if an invalid handle has been passed to a method, MCO_E_INVALID_HANDLE is returned. A function returning an error code causes the enclosing transaction to enter an error state.

The error state of the transaction is remembered by the runtime and any subsequent call to a runtime function within that transaction will not execute but simply return the MCO_E_TRANSACT code. Being aware of this can greatly simplify your application code, while keeping the code size to a minimum. For example, it is not uncommon (and many vendors recommend) to check the return code after every call to a library function. This leads to source code that looks like one of:

     
    int foo() 
    {
        int rc, i;
        for( i=0; i < 10; i++ )
        {
            if((rc=func1()) != 0)
                break;
            if((rc=func2()) != 0)
                break;
            if((rc=func3()) != 0)
                break;
        }
        return rc;
    }
     

or

 
    int foo() 
    {
        int rc, i;
        for( i = 0; i < 10; i++ ) 
        {
            if( (rc = func1()) == 0 )
                if( (rc = func2()) == 0 )
                    if( (rc = func3() == 0 )
                        . . .
                    else
                        break;
                else
                    break;
            else
                break;
        }
        return rc;
    }
     

In contrast, with eXtremeDB the application may simply check the return code on each iteration of the loop:

 
    uint foo() 
    {
        int rc, i;
        for( i = rc = 0; i < 10 && MCO_S_OK == rc ; i++ )
        {
            rc = func1();
            rc |= func2();
            rc |= func3();
        }
        return rc;
    }
     

The resulting implementation is clearly tighter code and easier to read.

Handling Fatal Errors

The third category of errors, fatal errors, are unrecoverable and cause the eXtremeDB runtime to call the internal function mco_stop(). This function performs the role of an assertion internal to eXtremeDB. If an error handler has been registered (via mco_error_set_handler()), mco_stop() will call this custom error handler. Otherwise mco_stop() will enter an infinite loop. If your application appears to “hang” inexplicably, it is probably because you have not registered an error handler, something went wrong, and mco_stop() entered this infinite loop.

The mco_stop() function is only called when the runtime detects an unrecoverable error, such as corruption within its metadata. In such a case, restarting the process is the only viable course of action. As well, any runtime function call can be asserted and mco_stop() called if the assertion fails. This usually means that the application did something illegal from the runtime’s point of view, such as passed an invalid transaction or object handle to a runtime function, or corrupted the runtime internals in some way.

Further, any runtime function might perform a number of validations that can result in a failed assertion. These validations vary depending on the CHECK_LEVEL set when the eXtremeDB library is compiled. The eXtremeDB object code packages include two sets of runtime libraries: the debug version, which has the highest CHECK_LEVEL and the release version, which has the lowest (minimal validations are performed). Although the release version does some validations, these have no negative impact on the overall performance. Developers are strongly advised to use the debug version during the development cycle. Then, only when no fatal errors are reported by eXtremeDB, switch to the release version. The only reason one would use the release version during the development phase is to measure application performance.

Debugging during the Development Cycle

The recommended method of debugging fatal errors for developers without a source code license is as follows:

The following example demonstrates this procedure (note that this code is taken from the 06_errorhandling_fatalerr sample):

 
    static void errhandler( int n )
    {
        printf( "\n eXtremeDB runtime fatal error: %d", n );
        getchar();
        exit( -1 );
    }
 
    void main()
    {
        ...
        mco_error_set_handler( &errhandler );
        ...
        rc = mco_trans_start(db, MCO_READ_ONLY, MCO_TRANS_FOREGROUND, &t);
        if ( MCO_S_OK == rc ) 
        {
            printf("\n\n\tThe following attempt to create a new record\n"
                "\tshould cause the Error handler to be called with Fatal\n"
                "\tError 340049 because it requires a READ_WRITE transaction.\n"
                "\tThe type of transaction started was MCO_READ_ONLY...\n"
                "\tNote: you will get error code instead of fatal error if\n"
                "\tthe program was linked not against _check runtime\n");
            /* anObject_new() should fail with error code 340049 = MCO_ERR_TRN+49 */
            rc = anObject_new(t, &rec);
            if ( MCO_S_OK == rc ) 
            {
                rc = anObject_data_put(&rec, data);
                /* the following code will not be reached unless the transaction is
                    changed to MCO_READ_WRITE */
                if ( MCO_S_OK == rc ) 
                {
                    rc = mco_trans_commit(t);
                }
            } 
            else if (rc == MCO_E_ACCESS) 
            {
                printf("\nThe sample was linked with no-check runtime\n");
                rc = MCO_S_OK;
            }
            ...
        }
    }
     

When the above code is executed it causes the error handler to be called with the error code 340049 which generates the following output:

 
    eXtremeDB runtime fatal error: 340049
     

Checking the error code, the value of 340000 corresponds to the constant MCO_ERR_TRN. This indicates an error in the transaction being performed. (The added value of 49 indicates the line within the runtime function where the assertion failed, causing mco_stop() to be called. This is useful if it is necessary to contact McObject Support – or if the developer has a source code license. For a more detailed explanation of error codes see C API Return Codes.)

Following is the call stack (as displayed by the Visual Studio 2008 debugger):

 
    06-errorhandling-fatalerr.exe!mco_w_new_obj_noid(mco_trans_t_ * t=0x004e0378, 
        unsigned int init_size=4, unsigned short class_code=1, mco_objhandle_t_ * ret=0x0012facc)  
        Line 494 + 0x14 bytes
 
    06-errorhandling-fatalerr.exe!anObject_new(mco_trans_t_ * t=0x004e0378, 
        anObject_ * handle=0x0012facc) 
         Line 120 + 0x2f bytes
 
    06-errorhandling-fatalerr.exe!main(int argc=1, char * * argv=0x00343250)  
        Line 60 + 0x13 bytes
         
    06-errorhandling-fatalerr.exe!__tmainCRTStartup()  
        Line 582 + 0x19 bytes
         
    06-errorhandling-fatalerr.exe!mainCRTStartup()  
        Line 399
         

It’s apparent that the function mco_w_new_obj_noid() called by anObject_new() caused the runtime assertion to fail. Knowing that error code MCO_ERR_TRN indicates a problem with the transaction and that the _new() functions require a READ_WRITE transaction, the solution is obvious.

Note that the eXtremeDB generated functions like anObject_new() interface with the runtime through “wrapper” functions like mco_w_new_obj_noid(). Though it is not necessary for developers to delve into the eXtremeDB internals, it can be instructive to examine the .c interface file generated by the mcocomp schema compiler and notice how the compiler generates the calling parameters to these “wrapper” functions from the corresponding “dictionary” values.

If you have a source code license, the debugging technique is slightly different. In this case it would be prudent to set a breakpoint in the mco_stop() function itself. This results in the following call stack:

 
    06-errorhandling-fatalerr.exe!mco_stop__(int n=340049, 
        const char * file=0x004d437c, int line=494)  
        Line 61
         
    06-errorhandling-fatalerr.exe!mco_w_new_obj_noid(mco_trans_t_ * t=0x004e0378, 
        unsigned int init_size=4, unsigned short class_code=1, mco_objhandle_t_ * ret=0x0012facc)  
        Line 494 + 0x14 bytes
         
    06-errorhandling-fatalerr.exe!anObject_new(mco_trans_t_ * t=0x004e0378, 
        anObject_ * handle=0x0012facc)  
        Line 120 + 0x2f bytes
 
    06-errorhandling-fatalerr.exe!main(int argc=1, char * * argv=0x00343250) 
         Line 60 + 0x13 bytes
 
    06-errorhandling-fatalerr.exe!__tmainCRTStartup()  
        Line 582 + 0x19 bytes
     
    06-errorhandling-fatalerr.exe!mainCRTStartup()  
        Line 399
         

Here again the preceding line in the call stack indicates that function mco_w_new_obj_noid() failed and the same chain of logic makes it clear that the solution is to correct the transaction type.