MTS Error Handling
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MTS Error Handling
Nick Drochak and Cristi Popp
Three-tier design and the Windows DNA architecture are touted as ways to make the developer's life easier, and if you can get everything right, the benefits are there. However, few of us get everything right the first time, especially when we are working with something new like COM DLLs and MTS. Nick Drochak and Cristi Popp present a way to find out what went wrong when you get a cryptic message from MTS like "OLE IDispatch exception code…blah, blah, blah."
When first working with COM DLLs running in MTS, we find that it's nigh impossible to debug them. In fact, it is impossible to debug the component interactively, as we're used to doing with the VFP IDE. What we need is a system that helps us debug MTS components.
This article will present the system that we use. It's a distributed error handling (DEH) system for trapping, logging, and returning errors as they occur in MTS components. The article will also show how XML makes it easy to collect and view information about those errors.
Note that in previous FoxTalk articles ("Reusable Tools: Error Handling Revisited," January 1998; "Best Practices, An Error Handling Class for VFP," July 1996), Doug Hennig described an excellent error handling system. However, it assumes that you're working with "normal" VFP objects (not COM) and can use message boxes and the like. Therefore, it isn't well-suited to COM objects. We developed this system to match the unique problems and constraints of COM and MTS.
In the following, we assume the reader has some knowledge of COM and MTS. Hopefully, the reader has built some COM objects and used them under MTS. Also, we'll discuss XML and Microsoft's XMLDOM object quite a bit. For readers who are unfamiliar with these technologies, please see the sidebar on page 3 for more information.
Setting the stage
A common scenario involves two or more tiers of objects hosted in MTS. (For the remainder of this article, when talking about "objects" or "COM objects," we're actually referring to COM objects running under MTS.) Although our DEH system is general enough to work with any number of tiers, for simplicity we'll assume a system (see Figure 1) composed of just two MTS objects: a Business Object (BIZO) and a Data Access Object (DAO). In this system, the BIZO takes care of business rule validations, and the DAO handles data storage and retrieval. The base client is a VFP front-end application, and the back-end database could be SQL Server, Oracle, or, of course, VFP.
Our goal is to receive information about errors that occurred in middle-tier objects. In order to do so, each object's COM methods follow these basic steps:
- Store information about every local error, as it occurs, into an XML string.
- If the object uses other objects, concatenate any error XML received from them with its own.
- Before exiting, store the local errors collected in step 1 to a log file.
- Pass the resulting error XML from step 2 to its caller.
We developed a base class that performs all of these steps. All of the code we present later comes from this class. We used it as the base class for all of our middle-tier objects. From these objects, the base client (BC) receives one XML string that contains all of the errors that occurred in all middle-tier objects (see Figure 2). The BC can use the IE5 Web browser control to display this error information in an easy-to-read format.
Step 1-Collecting local errors
The first step is to put code into the middle tier's base class's Error() method. This code constructs an XML string describing all of the errors the object encountered during the execution of the current COM method. The first question that arises is, "How should we structure the XML data?" There could be multiple tiers, each potentially composed of objects of many different classes. Consequently, we decided the information in the XML string should be organized in a hierarchy of elements like Listing 1 (Figure 3 shows how the XML looks in IE5):
Listing 1. Error XML's structure.
Errors Tier Class Action Error1 ErrorN
XML requires one "top" element, and ours is called "Errors." Under it, "Tier" represents elements corresponding to the tiers of an n-tier application, such as "BIZO" and "DAO." "Class" is the name of the VFP subclass used for the object. "Actions" might include elements like "Save" or "Delete," which potentially encompass several methods. Because Error() knows only the name of the method that generated the error, we found the concept of "Action" to be useful. It provides a logical context for understanding the environment that led to the error. During the course of each "Action," several errors might occur. We store detailed information about each error in elements "Error1" through "ErrorN," under the appropriate action element.
The base class has properties that denote the tier, class, and action. The Error() method uses these properties to build the XML string, which is kept in a property called "cXML."
Astute readers might notice that we don't include the name of the COM method in the hierarchy. We found the "Action" more useful, and anyway we include the call stack with each ErrorN element. The stack of called method names (Calling Context) can be easily created as a string by using the VFP function Program(), using code like this (BuildCallingContext() is called by the Error() method, as are all of the methods shown here in Step 1):
* Method: BuildCallingContext() local lnCounter, lnMethodName, lcCallingContext lcCallingContext = '' * 0 and 1 give the master program lnCounter = 2 lcMethodName = program(lnCounter) do while NOT Empty(lcMethodName) * use backslash to make it look like a path lcCallingContext = addbs(lcMethodName) + ; lcCallingContext lnCounter = lnCounter + 1 lcMethodName = program(lnCounter) enddo return lcCallingContext
Here's the segment of the code from the Error() method that deals with building the XML string. First, it makes an XMLDOM object and loads the contents of the middle-tier object's cXML property string, which might already contain error information from an error in another object or this one. By doing this, the XMLDOM converts the string representation of the XML into a hierarchy of node objects that we can easily manipulate with code. Then, we can retrieve a string representation of the XML contained in the DOM by reading its "xml" property.
* create the XMLDOM object loXMLDOM = createobject('Microsoft.XMLDOM') * have LoadXML wait until it gets the whole string loXMLDOM.async = .f. * load whatever is already in the cXML property loXMLDOM.LoadXML(this.cXML)
When an error occurs, the Error() method inserts information about it into the XML string. It needs to manipulate the XML string and maintain the tree-like structure of Listing 1. It also needs to insert the information at the correct place in the tree. Therefore, it first looks for an already existing Tier element corresponding to the object's tier. If an element doesn't exist, one is created. The same process of locating/creating occurs down the hierarchy for the Class and the Action elements as well. Here's the code, from Error(), that "beams" us to the correct place in the tree, by locating the appropriate Action node:
loErrors = this.GetXMLElement(loXMLDOM, 'Errors') loTier = this.GetXMLElement(loErrors, this.cTier) loClass = this.GetXMLElement(loTier, this.class) loAction = this.GetXMLElement(loClass, this.cAction)
The cAction property is usually set in code by the developer whenever a new logical action starts.
Once we have the correct Action element, an Error element for the current error is added under it as a child, with a date-time attribute. The error's number, description, calling context, and line number are added as sub-elements. Here's the segment of code from the Error method that accomplishes this task:
*** add this error i = loElemAction.ChildNodes.length loElemError = loXMLDOM.CreateElement( ; 'Error' + alltrim(str(i))) loElemAction.AppendChild(loElemError) loElemError.SetAttribute('DateTime', datetime()) *** add all properties of the error * tnError is a Error() method parameter this.SetErrorSubElement(loXMLDOM,loElemError, ; 'Number',tnError) * lcDescription comes from aError() this.SetErrorSubElement(loXMLDOM,loElemError, ; 'Description',lcDescription) this.SetErrorSubElement(loXMLDOM,loElemError, ; 'CallingContext',this.BuildCallingContext()) * tnLine is a Error() method parameter this.SetErrorSubElement(loXMLDOM,loElemError, ; 'Line',tnLine)
Finally, the Error() method stores the XML string back into its cXML property in case there's another error later. The code is simple:
this.cXML = loXMLDOM.xml
In case you're missing the forest for the trees, here's a "layman's" explanation of what the Error method does. The middle-tier object's cXML property has a value of some sort (either empty or already has some errors). As another error occurs, we bop out to the XMLDOM (with the original value of cXML) to manipulate the error, and then stuff the result-the original value plus the new error info-back to cXML.
We showed the Error() method using GetXMLElement(). This method tries to locate and return an element with a given name under a given parent. If the element doesn't exist, the method creates it. Here's the code (toParent is the element under which the search is performed, and tcName is the name of the element to locate):
* Method: GetXMLElement() lparameters toParent, tcName local loCollection, loElement loCollection = toParent.GetElementsByTagName(tcName) * if not found, length will be 0 if loCollection.length=0 loElement = toParent.CreateElement(tcName) toParent.AppendChild(loElement) else loElement = loCollection.item(loCollection.length-1) endif return loElement
Step 2-Concatenating "outer" XML strings
In the general case of an n-tier application, a middle-tier object may receive error XML from COM objects it uses. It needs to combine the error XML it received ("outer" XML) with its own ("inner" XML). This should be done after every call to an "outer" COM method. Here's an example:
* example COM method from a middle tier object loOuterObj = createobj('mylib.myobj') loOuterObj.SomeMethod(lnParam1, lnParam2, @lcOuterXML) this.ConcatenateXMLs(this.cXML, lcOuterXML)
Both XMLs might contain information from multiple tiers and classes. Care needs to be taken when copying error elements from one tree to another so that the Tier/Class/Action/Error hierarchy is preserved. Notice that each error element lies under a unique Tier/Class/Action tuple (see Listing 1). To copy an error element, you must place it under the exact same tier, class, and action in the destination tree. However, those tiers, classes, and actions might not appear in the destination tree. In that case, the elements are first created there.
Here's the code that performs the actual concatenation. This code is in our base class, so the subclass gets it for free. For performance reasons, the code grafts the tree of local errors into the (usually) larger tree of outer errors. It uses two XMLDOM objects (see the sidebar on page 3), which are loaded with error XML strings. loXMLDOMInner is loaded with the inner XML, and loXMLDOMOuter with the outer XML. The resulting XML is stored in the object's cXML property:
* Method: ConcatenateXMLs() lparameters tcSrc, tcDest= loXMLDOMOuter = createobject('Microsoft.XMLDOM') loXMLDOMOuter.async = .f. loXMLDOMOuter.LoadXML(tcDest) * create loXMLDOMInner the same way, load with tcSrc loElemErrors = ; this.GetXMLElement(loXMLDOMInner, 'Errors') for each loInnerTier in loElemErrors.ChildNodes for each loInnerClass in loInnerTier.ChildNodes loOuterClass = this.GetClass( ; oXMLDOMOuter, loInnerTier.NodeName, ; oInnerClass.NodeName) this.CopyXMLBranch( ; oInnerClass, loOuterClass) endfor endfor this.cXML = loXMLDOMOuter.xml
The ConcatenateXMLs() method uses GetClass() to locate (or create) a class element in the outer XML that's under a certain tier element. This is merely a wrapper method for three successive calls to GetXMLElement() (which was described in the "Step 1" section):
* Method: GetClass() lparameters toDOM, tcTier, tcClass loErrors = this.GetXMLElement(toDOM, 'Errors') loTier = this.GetXMLElement(loErrors, tcTier) loClass = this.GetXMLElement(loTier, tcClass) return loClass
For each class found in the inner XML, ConcatenateXMLs() copies its error information into the outer XML by using CopyXMLBranch().
* Method: CopyXMLBranch() lparameters toSrcClass, toDestClass for each loSrcAction in toSrcClass.ChildNodes loDestAction = this.GetXMLElement( ; toDestClass, loSrcAction.NodeName) for each loSrcError in loSrcAction.ChildNodes loDestError = loSrcError.CloneNode(.t.) loDestAction.AppendChild(loDestError) endfor endfor
Step 3-Logging to disk
This step assumes all of the errors have been collected into the cXML property and all that's left to do is store it on disk. Disk logging is always done just before exiting a COM method. In the design we chose, each tier has its own log file, and the name of the log file is stored in a property of the base class. The path used for this file is the value from home(), which is the location of the DLL on disk.
The logging process is straightforward. It uses a slightly smaller version of ConcatenateXMLs() to append just the "branch" of this object's tier and class from the local XML (which might contain many tiers and classes) to the log file's XML. Then the log file is overwritten. However, there are two issues with which we need to deal:
- Imagine a situation in which two COM objects using the same log file both wish to log their errors. Object A extracts the XML from the file and concatenates it with its own XML error string. In the meantime, Object B reads the XML from the file. Object A then overwrites the file. Object B now concatenates the original XML from the file with its own, and overwrites the file. However, in so doing the information from Object A is lost. It's obvious that the COM objects need to lock the file before reading it, and release the lock after writing to it.
- The log file's size will grow, and it's always loaded into an XMLDOM object before concatenating it with the local XML string. If the file is quite large-say, several megabytes-this can waste time and resources.
To address issue 1, we used MTS's Shared Property Manager (SPM). It allows you to implement a locking mechanism (critical section) for threads of the same process (package in MTS). We didn't address issue 2 in this version, but one idea would be to establish a maximum size for the file. If the file is larger than this size, then rename it using the current datetime() and start a new log file bearing the original name. This renaming would need to occur within the same critical section as well.
Here are excerpts from the implementation of the LogErrors() method that show the usage of the SPM and the way the error information is transferred from the source to the destination (in this case, the XML source is the object's cXML property and the destination is the log file's XML):
* Method: LogErrors() loSharedGroupManager = this.oContext.CreateInstance( ; "MTxSpm.SharedPropertyGroupManager.1") if vartype(loSharedGroupManager)='O' lnIsolationMode = 1 &&LockMethod lnReleaseMode = 0 &&Standard * create/get reference to the <<Property Group>> * inside which lies the property you want: loSharedGroup = ; loSharedGroupManager.CreatePropertyGroup( ; this.cTier, lnIsolationMode, ; lnReleaseMode, @llExists) loSharedErrorLogFile = ; loSharedGroup.CreateProperty( ; this.class, @llExists) endif * load XML strings into the XMLDOMs * copy this class' XML branch to destination: loSrc=this.GetClass(loDOMSrc,this.cTier,this.Class) loDest=this.GetClass(loDOMDest,this.cTier,this.Class) this.CopyXMLBranch(loSrc, loDest) * write to file the resulting XML string
By setting IsolationMode to LockMethod (1), we ensure that no other object will access the property group for the duration of the current COM method. After the current COM method exits, the SPM will release the lock on the property group, therefore enabling access to the log file as well. Setting ReleaseMode to Standard (0) means the property group is automatically destroyed when all clients have released their references to it.
We preferred to implement the critical section by means of the MTS SPM because it's language independent. Considering the fact that the log file belongs to the tier rather than to a certain COM object, it's possible other COM objects written in VB or VC++ could coexist in the same tier as the VFP COM objects. File I/O contention issues can be handled easily in the same way for all these objects (as long as they're in the same package), without worrying about language differences.
Step 4-Returning the errors to the caller
The COM objects need to pass the XML error string back to their caller. Your first thought might be, "It's obvious, use ComReturnError()." That was ours, and we used it in the first version of our system. However, we found a problem that limits its usefulness.
COMReturnError() returns control directly to the base client. If you're using multiple tiers of COM objects, that isn't the behavior you'd like. The objects from the intermediate tiers don't get a chance to react to those errors. Our design required that each object regain control after making a call to another object. Also, it seems that COMReturnError()'s string parameters are limited in size to about 512 characters, which was unacceptable for our purposes.
Since COMReturnError() didn't fit our requirements, we came up with another solution. We used a parameter passed by reference for returning the XML error string. For example, have a look at this code from a client application:
local lcXMLErrors, loRecordset * avoid COM marshalling errors lcXMLErrors = '' * try to get some records loRecordset = loBizObj.GetEmployees(@lcXMLErrors) if !empty(lcXMLErrors) * analyze the XML received * display it in an IE5 Web Browser control * log it to disk endif
An important point is that we have to set lcXMLErrors to a string value. If it's left as a logical, then COM will raise an error when it tries to return a character-type value. This problem is related to "parameter marshalling" in COM, and it can look like an error occurred in the method. However, in truth, it's an error when COM tries to send the parameters.
A pattern for COM methods
Putting together all of the steps listed so far, we established a pattern for our COM methods. Here's a look inside a typical middle-tier method-GetEmployees() of a hypothetical business object:
lparameters tcXMLErrors && By Reference This.cAction = 'GetEmployees' * do a bunch of stuff here loSomeRecords = loOtherCOMObj.Fetch(@lcXMLErrors) this.ConcatenateXMLs(this.cXML, lcXMLErrors) * do more stuff here this.LogErrors() * SetComplete will reset our property tcXMLErrors = This.cXML this.oContext.SetComplete() return loSomeRecords
Notice that errors could have occurred either in the object itself or in the object it used. If the former, then the Error method is automatically invoked, and the object's cXML will be filled with the error string. This is because the Error method is inherited from the base class. If the latter, lcXMLErrors will be filled by that object with an XML string, which this object then merges with its own. During the life of this object (in MTS this usually means the call to one COM method), its cXML property will accumulate all of the errors that occurred in its own methods and in other middle-tier objects that it used. It will be able to pass this string back to whoever called it, be that the base client or some other middle-tier object. And this is the pattern that our COM methods follow:
- Set the action property whenever appropriate
- Do whatever processing is needed
a. Call another COM object's method
b. After each call, ConcatenateXMLs()
- Log local errors to disk once the method finishes its job
- Copy the cXML property into the error output parameter
- SetComplete (or SetAbort as appropriate)
The COM method can decide to SetAbort() or SetComplete() based on any errors in the string, as some errors might not be serious enough to abort. This gives the component designer a lot of flexibility.
It's up to the caller to decide what to do based on what's in the XML string. For example, before displaying it, the client application might want to analyze the XML received in order to make data-driven decisions.
Client application's job
The best part about this error handling system is how easy it is to view the errors once we get the XML string back in the client app. We make a simple debugging tool using a form with IE5's Web browser control, and load the string into the control. That's it.
There's much more the client application could do with the XML it receives. For example, once the application is in production, it would be useful to have the XML logged on the client's disk. The XML received by the base client (BC) contains all of the errors encountered on the way to the data and back. The support staff could easily identify the cause of the errors, simply loading the client's XML log file into IE5 by just double-clicking on it. Provided the BC also uses an object derived from our COM base class (see section "System Overview"), the client application's code would look like this:
loRecordset = loBizObj.GetEmployees(@lcXMLReceived) if !empty(lcXMLErrors) * analyze the XML received thisform.ie5.LoadXML(lcXMLErrors) * load XML from log file into lcXMLLogged this.ConcatenateXMLs(lcXMLReceived, lcXMLLogged) endif
Using the XMLDOM object, the client application could also analyze the XML data, restructure or filter it before displaying, and so forth.
The general case
We started our discussion assuming a simplified example of a system with two middle-tier objects. However, the architecture of our DEH system allows for any number of tiers and objects. Figure 4 (on page 7) illustrates an n-tier application, and how the DEH system fits into it.
We're currently implementing an improved version that uses a separate stand-alone error handling MTS object. It will be faster and language independent (but written in Fox, of course). It uses, on a much larger scale, the SPM and MSMQ in order to speed up the error handling process and scale better than the current implementation. We hope to present it in a future article.
Until then, we hope that these XML error strings help you to debug your MTS components. Let us know if you have any ideas that we could use. Constructive criticism is always welcome.
Nick Drochak has been programming in various incarnations of FoxPro for over seven years. He's an MCP who is currently working in Tokyo as manager of East West Technology Services, Inc. There, he and his team are using the latest Microsoft acronyms, including VFP, SQL Server, VB, VC++, MTS, and ADO. 81-3-3222-5531, firstname.lastname@example.org.
Cristi Popp is a senior software engineer at East West Technology Services, Inc. and an MCP. He's developing software using mainly C++, VFP, and SQL Server. His interests include OOD, OOP, distributed software architectures, and math. 81-3-3222-5531, email@example.com.
Sidebar: XML and IE5
When you install Internet Explorer version 5.0 (IE5), you get the Microsoft Web Browser as an ActiveX control. This control can be dropped on a form and used to display XML in an easy-to-read format. You also get the XMLDOM (XML Document Object Model), which is a COM object that you can use as a tool to manipulate XML data. We used them both in our error handling system. Because the browser shows XML in a nice tree-like structure, you can arrange the error information in a hierarchy so that it's easy to find specific errors. It would be simple to use an XSL style sheet to format the error information, but that discussion is beyond the scope of this article. Microsoft's Web site has good information on XML, XSL, and the XMLDOM at http://msdn.microsoft.com/XML.
It can be tricky to find the Web browser control among all of the other ActiveX controls installed on your system. It's simply named, "Microsoft Web Browser." The XMLDOM object has a ProgID of "Microsoft.XMLDOM." Once you do find the control and put it on a form, there's one thing you must do to be able to run the form (thanks to Ken Levy at DevCon '99 for this tip). In the Refresh() method of the browser control, include NODEFAULT. If you forget, you'll get this error message: "OLE error code 0x80004005: Unspecified Error" every time the control is refreshed.
To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57
Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.
This article is reproduced from the March 2000 issue of FoxTalk. Copyright 2000, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.