Troy Cambra
Support Engineer
Microsoft Corporation
June 19, 1998
Contents
Introduction
Why Write Components?
Why Use Microsoft Transaction Server?
Why Use Visual Basic?
Using the MTS Objects
Using the ASP Intrinsic Objects
Performance and Process Isolation Tips
Tips for Building MTS Components
Debugging Tips
The integration of Microsoft® Internet Information Server (IIS) 4.0 with Microsoft Transaction Server (MTS) 2.0 yields a powerful server environment. The integration provides process isolation, transactional Web page support, and a rich framework for building components into IIS. This article describes how to build components that take advantage of the new features in IIS and MTS, and provides an extensive list of tips on building components, performance and process isolation, and debugging.
This article assumes that you have a solid working knowledge of Internet Information Server, Active Server Pages (ASP), Microsoft Transaction Server, Visual Basic® 5.0 and the Component Object Model (COM) . You can install IIS 4.0 and MTS 2.0 from the Windows NT® Option Pack, which is available from http://www.microsoft.com/windows/downloads/default.asp .
With ASP, it is entirely possible to build rich applications using only the features provided in IIS 4.0. You can access context, perform transactions, access databases, and so forth, without having to write your own components. So why, you might ask, would you want to write components? The three most compelling reasons are:
Performance and scalability. Executing native code is faster and typically less memory-intensive than interpreting scripts.
Business logic encapsulation. This creates better code organization, and also improves code debugging, distribution, and upgrading. In addition, you can reuse components within and across your applications.
Separation of data and user interface (UI). Components make it easy to add a fresh UI to applications without having to touch business logic and data. This is important in a fast-changing Web-based application.
For more information on components, see:
Enabling the Use of Out-of-Process Components in ASP Using the Metabase
Web Workshop's Component Development area (on the home page, click the "show contents" link.)
If you are using distributed transactions on your site, MTS is the obvious choice. However, even if you don't use transactions or databases, you will find that MTS is a very useful environment in which to run your components. MTS provides object brokering and run-time services as well as transaction monitoring. Consider MTS if your components require any of the following:
For more information on MTS, see:
IIS 4.0 and MTS 2.0: Technology for the Web
Any language capable of creating apartment-threaded in-process COM components is suitable for developing MTS components. The choice of language should be primarily based on your familiarity with it and its suitability for the task. Familiarity with a language is the most important factor. Knowing how to use a language correctly and to its greatest efficiency and power can determine, in large part, the success of a project. However, suitability for the task also needs to be considered. For example, if raw speed were of utmost importance, a lower-level language such as C++ would be the better language to use.
Visual Basic is a solid tool for most applications. It is mature, stable, and proven. It is a sound rapid application development (RAD) tool, but has enough flexibility to get down to the underlying APIs and enable the developer to tweak performance or provide advanced features.
An MTS component is simply an ActiveX® DLL project. Any class in an ActiveX DLL project can potentially be an MTS component. However, to take full advantage of the MTS features, it is necessary to use the MTS programmer's interface. The first step in doing this is to reference the Microsoft Transaction Server Type Library. This enables access to the MTS objects, the most important of which is ObjectContext. ObjectContext provides access to your object's MTS context, which provides the core MTS functionality.
In MTS, a logical thread of work is called an activity. An activity can span multiple objects and multiple method calls to any object. An activity's scope spans from the first call from the base client until either the root object calls ObjectContext.SetComplete and exits the method or the base client releases the root object. Although it is possible to have activities that span multiple method calls on any object in the activity, it is best to limit it to a single method call per object if possible. If this is done, you would simply obtain context in the method and release it before exiting. The code would look something like this:
Public Sub SomeMethod() Dim objCtx as ObjectContext Set objCtx = GetObjectContext If objCtx Is Nothing Then _ Err.Raise 91 'error if failed 'Perform work. 'VB will release the 'reference upon method exit End Sub
If the activity will span several method calls and the calls all require access to the object's context, it is generally a good idea to store it in a class member variable. It is bad form to obtain and release the object's context in the Initiate and Terminate events. These events should be considered the constructor and destructor of the class, so context should not be assumed to be available. This is not always the case for Visual Basic, but should be followed for consistency with all other languages, for better error handling and reliability.
The best way to obtain and release context is to use the ObjectControl interface methods. ObjectControl provides a solid mechanism for context handling, and also allows the component to take advantage of object pooling when it becomes available in future releases.
Code using the ObjectControl interface methods to handle context in the class module might look something like this:
Option Explicit 'Private member variables Private m_objCtx As ObjectContext 'Public class functions 'Implementation of ObjectControl interface Implements ObjectControl Private Sub ObjectControl_Activate() Set m_objCtx = GetObjectContext If m_objCtx Is Nothing Then _ Err.Raise 91 'error if failed End Sub Private Function ObjectControl_CanBePooled() As Boolean ObjectControl_CanBePooled = False 'don't pool for now End Function Private Sub ObjectControl_Deactivate() Set m_objCtx = Nothing 'release End Sub 'Actual public class functions 'TBD
Once the context is obtained, you can use it to control transaction outcome and object recycling, to query context properties, and to perform various functions. The following function shows a typical use of the context properties and methods:
Public Function BasicFunction(ByVal blnAudit As Boolean) As Variant 'sample MTS function Dim strAuditString As String Dim strErrString As String 'This function will optionally log auditing information On Error GoTo ErrHand If blnAudit Then strAuditString = "Method: BasicFunction " & vbCrLf & _ "Thread ID: 0x" & Hex(App.ThreadID) & vbCrLf & _ "Transactional: " & m_objCtx.IsInTransaction & vbCrLf & _ "DirectCaller: " & m_objCtx.Security.GetDirectCallerName & vbCrLf & _ "OriginalCaller: " & m_objCtx.Security.GetOriginalCallerName Call App.LogEvent(strAuditString, vbLogEventTypeInformation) End If 'Perform the actual work 'Actual work TBD 'Finish up BasicFunction = "Something Wonderful Happened" 'Commit the transaction m_objCtx.SetComplete Exit Function ErrHand: 'log the actual error strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _ "Error Description: " & Err.Description & vbCrLf & _ "Error Source: " & Err.Source On Error Resume Next Call App.LogEvent(strErrString, vbLogEventTypeError) 'Abort the transaction - if one m_objCtx.SetAbort 'Raise a friendly error to the caller. Minimizing the number of 'errors returned to the client makes programming at that level 'easier. For example, you typically have two types of errors 'recoverable and non recoverable. You could simply send back 'one or the other so the caller can easily make a decision 'about how to handle it. Call Err.Raise(vbObjectError + 1001, _ "Basic Component", _ "Basic Error. Please contact the system administrator") End Function
The ASP intrinsic objects are available within MTS objects through the object's context. Making use of the ASP intrinsic objects from within an MTS object is simple and can be quite effective. For example, if you have an ASP page that is building a complicated HTML page using the Response object, you can encapsulate the code in a component that will execute much faster, and be much easier to debug. Here is a sample function from a component and a sample ASP script that calls it:
The Component
Public Function ASPVarUse() As Variant 'sample mts function Dim strErrString As String Dim objApplication As Object Dim objResponse As Object Dim objRequest As Object Dim objServer As Object Dim objSession As Object Dim objItem As Object 'Get the IIS intrinsic objects If m_objCtx.Count < 5 Then _ Call Err.Raise(vbObjectError + 1002, "Basic Function", _ "This object is meant to be called by ASP pages only") Set objApplication = m_objCtx.Item("Application") Set objResponse = m_objCtx.Item("Response") Set objRequest = m_objCtx.Item("Request") Set objServer = m_objCtx.Item("Server") Set objSession = m_objCtx.Item("Session") 'Access the ASP objects objResponse.Write "<p> Outputting from the MTS component </p>" Call objServer.HTMLEncode("The paragraph tag: <P>") objApplication("AppTest") = "Application Test" objSession("SessionTest") = "Session Test" For Each objItem In objRequest.QueryString("RequestTest") objResponse.Write objItem & "<BR>" Next ASPVarUse = "Successful Test" 'Commit the transaction m_objCtx.SetComplete Exit Function ErrHand: 'log the actual error strErrString = "Error Code: 0x" & Hex(Err.Number) & vbCrLf & _ "Error Description: " & Err.Description & vbCrLf & _ "Error Source: " & Err.Source On Error Resume Next Call App.LogEvent(strErrString, vbLogEventTypeError) 'Abort the transaction - if one m_objCtx.SetAbort 'Raise a friendly error to the caller. Call Err.Raise(vbObjectError + 1001, _ "Basic Component", _ "Basic Error. Please contact the system administrator") End Function
The ASP Script
<% Dim objTest On Error Resume Next 'Create the MTS object Set objTest = Server.CreateObject("PMTSIISVB.IMTSIISVB") If Err.Number <> 0 Then Response.Clear Response.Write "<p> " & Err.Description & " </p>" Response.End End If Response.Write "<p> Starting Test </p>" Response.Write "<p> ASPVarUse Return: " & objTest.ASPVarUse() & " </p>" If Err.Number <> 0 Then Response.Clear Response.Write "<p> " & Err.Description & " </p>" Response.End End If Response.Write "<p> Session Variable = " & Session("SessionTest") & " </p>" Response.Write "<p> Application Variable = " & Application("AppTest") & " </p>" Response.Write "<p> Ending Test </p>" %>
Balancing fault tolerance, programming ease, and performance can be tricky. There are countless things that can be done to fine-tune performance. I've put together a list of some of the more common things you can do or avoid doing to improve performance. For a more detailed discussion of IIS and ASP performance tuning, please see the Internet Information Server Resource Kit .
It is important to fully analyze the entire server. You might think that running every ASP page in every application or virtual Web site in-process would create better performance than running some or all out-of-process. This is not always true. There are literally thousands of variables in the performance equation. The only way to be sure of optimum performance is to plan performance testing into your application development cycle. For example, I have had some larger Web sites actually see a substantial performance increase by moving applications out-of-process.If process isolation between the application and the IIS server is important, run the IIS application in a separate process, but use a library package for the MTS components. This is typically a good tradeoff. A problem in a component could bring down the application, but even if they were in two different processes, the problem would probably propagate back to the ASP in some form anyway. Further, this configuration will typically outperform running the ASP in the IIS process and the MTS components in a server process.
ASP pages are going to use the IDispatch interface of the component. This causes two calls per method call. If you use early binding in your intermediate component, you can limit the network calls to one call per method. Further, you can design your intermediate component to hold references, cache data, and so forth. This can minimize the amount and frequency of data accessed over the network.
Here is a list of suggestions for building MTS components for use in IIS. This is not a set of rules, but flexible guidelines.
Visual Basic has a strong interpreted debugger in the integrated development environment (IDE). Unfortunately, this doesn't help when you're trying to run in the MTS environment. When running in MTS, it is necessary to debug the executable code directly. Although Visual Basic is not capable of debugging MTS components directly, it can generate debugging symbols that can be used in the Visual C++® debugger (Developer Studio). Although limited compared to the Visual C++ debugging functionality, this is still one of the best ways to debug Visual Basic components in MTS.
While the Visual C++ debugger is recommended because it's easy to use, the latest WinDGB.EXE can also be used to debug Visual Basic components. WinDBG is more difficult to use, but is in some ways more powerful. It is also free. The latest version is available from http://msdn.microsoft.com/developer/sdk/windbg.htm.
Any language that uses a large run-time or virtual machine (such as Visual Basic or Java) can make tracking bugs more difficult, because there's a lot going on below the level at which you are working. Further, the use of libraries that wrap underlying APIs such as ActiveX Data Objects (ADO), Remote Data Objects (RDO), and Oracle Power Objects can further complicate things. It would be nice to think that they are bug-free, but the reality is that they are not -- and finding those bugs without source code and symbols is very difficult. Finally, the run-times and wrappers can do things that are programmatically convenient but cause performance or functionality problems in the MTS environment. One such example is the querying of the database system tables that many of the data-access objects perform. This can make programming easier, but can also cause serious blocking in the database.
It is not impossible to properly debug Visual Basic components, but it is a little more difficult than some other kinds of debugging. Extra care must be taken to assure robustness. I typically follow the these guidelines when building and debugging an MTS component in Visual Basic:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Transaction Server\Debug\RunWithoutContext
This allows the code to run as if it were in MTS. See the "Debugging Visual Basic MTS Components" help topic in the MTS help file for details on this setting.
Use tracing and assert "macros" throughout the code. The built-in Visual Basic support for debugging will not work when using the Visual C++ debugger. The following debugging functions can be very useful when debugging in this manner:
#If DEBUGGING Then 'API Functions Public Declare Sub OutputDebugStringA _ Lib "KERNEL32" ( _ ByVal strError As String) Public Declare Function MessageBoxA _ Lib "USER32" ( _ ByVal hwnd As Long, _ ByVal lpText As String, _ ByVal lpCaption As String, _ ByVal uType As Long) As Long Public Declare Sub DebugBreak Lib "KERNEL32" () Public Declare Function IsDebuggerPresent Lib "KERNEL32" () As Long 'API Constants Private Const API_NULL As Long = 0 Private Const API_FALSE As Long = 0 Private Const API_TRUE As Long = 1 Private Const MB_ICONERROR As Long = &H10 Private Const MB_SERVICE_NOTIFICATION As Long = &H200000 Private Const MB_YESNO As Long = 4 Private Const IDYES As Long = 6 Private Const IDNO As Long = 7 'Other Constants Private Const ASSERT_DIALOG_STRING As String = _ "The Component has thrown an assert." _ & vbCrLf & "Press Yes to debug the application." _ & vbCrLf & "Press No to continue running the application" Private Const ASSERT_DIALOG_TITLE As String = "Component Assert" Private Const DEBUG_DIALOG_TITLE As String = "Component Debug Message" #End If 'Exposed functions Public Sub DebugPrint(ByVal strError As String) #If DEBUGGING Then Call OutputDebugStringA(strError) #End If End Sub Public Sub DebugMessage(ByVal strError As String) #If DEBUGGING Then Dim lngReturn As Long lngReturn = MessageBoxA(API_NULL, strError, _ DEBUG_DIALOG_TITLE, _ MB_ICONERROR Or MB_SERVICE_NOTIFICATION) #End If End Sub Public Sub DebugAssert(ByVal vntExpr As Variant) #If DEBUGGING Then Dim lngReturn As Long Dim blnAssert As Boolean On Error GoTo ExecAlways blnAssert = True Select Case VarType(vntExpression) Case vbEmpty blnAssert = True Case vbNull blnAssert = True Case vbString If vntExpr = vbNullString Then blnAssert = True Else blnAssert = False End If Case vbObject If vntExpr Is Nothing Then blnAssert = True Else blnAssert = False End If Case vbError blnAssert = True Case vbBoolean blnAssert = Not vntExpr Case vbDataObject If vntExpr Is Nothing Then blnAssert = True Else blnAssert = False End If Case Is > vbArray blnAssert = False On Error GoTo ErrArray lngReturn = UBound(vntExpr) 'assert if empty On Error GoTo ExecAlways Case Else 'numeric, byte If vntExpr = 0 Then blnAssert = True Else blnAssert = False End If End Select ExecAlways: If blnAssert = True Then If IsDebuggerPresent = API_TRUE Then Call DebugBreak Else If IDYES = MessageBoxA(API_NULL, ASSERT_DIALOG_STRING, _ ASSERT_DIALOG_TITLE, _ MB_YESNO Or MB_ICONERROR Or MB_SERVICE_NOTIFICATION) Then Call DebugBreak End If End If End If Exit Sub ErrArray: blnAssert = True Resume Next #End If End Sub
Use the DebugAssert() "macro" throughout the project code. The other two functions are less useful, but can be useful when trying to track a stress-related bug. When building a debug version, define a conditional compile argument as DEBUGGING=-1, and compile. When an assert is hit, the debugger should break the application. When compiling for release code, set the conditional compile argument to DEBUGGING=0 or remove it completely.
The lack of true macro capability in Visual Basic means that even in the release code there is still a method call for each of the above debug calls. The method call does nothing, but there is still some overhead in making the call. If performance is of utmost concern, it is possible to conditionally compile the debug calls so that they are not part of the release code:
#If DEBUGGING Then DebugAssert(SomeValue) #End If
Sometimes there are bugs that the above simply does not catch. Tracking down hangs and performance problems is always fun. There are two things to look at first when doing so: CPU utilization and database blocking. If CPU utilization is normal on both the MTS server and the database server, blocking in the database is often the cause. Finding the underlying cause typically requires looking at the database and ODBC logs and traces. If the CPU utilization is peaking on one machine, start looking there for bottlenecks. There is no simple solution to this problem. You have to look at everything: it could be disk utilization on the database server, an indication that the objects are too large, simply an underestimate of necessary server hardware, and so forth -- all a little beyond the scope of this paper.
Tracking down sporadic exceptions is yet another time-consuming and less than enjoyable experience. The MTS, ASP, or COM environments typically handle exceptions by trapping the error, logging the event, and cleaning up as necessary. It is possible to catch these by running the application with a debugger attached. Although you can debug these, you can also simply call product support. Be prepared to install symbols, debuggers, and even RAS on the machine.
Debugging components from ASP pages adds yet another layer of difficulty. However, the Script Debugger and the above debugging code can make it much easier to debug problems right in the application process. Copying the ASP code into a Visual Basic project and creating a client executable that can then be used to debug the component can be a useful (and sometimes necessary) trick. This can be especially useful when developing ASP scripts and Visual Basic components. Overall, Visual Basic is still the best editor for creating and testing Visual Basic code.