The starting point for package design is the physical design, as discussed in Chapter 6. During the application design phase, the primary goal was to document any constraints imposed by application requirements, such as requiring authorization checks on calls into particular components or requiring certain components to run on a particular computer. It was not necessary, or even desirable, to have the entire physical design completed before implementing components. As applications are implemented, their physical design should be refined, with components placed into packages and any package deployment constraints documented. These four issues should be kept in mind when deciding how to package components:
We look at each of these issues in detail in the following sections.
An application servers' components can be activated in one of two ways: in the client's process or in a package-specific MTS-managed process. We've already seen that MTS library packages are used to run the components in the client's process. Note that the term "client" in this sense is a different component or application running on the same computer on which the library package is installed, rather than a user application. Library packages do not support declarative security, nor do they offer the benefits of process isolation. Thus, library packages are typically used for utility components that might be used by multiple applications. Library packages are also useful when separate process overhead is undesirable and authorization checks are not required.
MTS server packages are used to run components in a separate MTS-managed process. MTS server packages also support declarative security, resource pooling, and so on. Most MTS packages are server packages. Application components are grouped into packages based on common resource usage, security requirements, and fault isolation needs.
Deciding how many packages are needed in an application is a balancing act. With many packages, system administrators have a lot of flexibility in deploying their applications. However, each MTS server package requires a certain amount of overhead to manage resource pools, shared properties, and so on. In addition, inter-process COM calls are much more expensive than in-process COM calls within a single apartment. And too many packages make system management tough. At this point in their application development, developers should create packages they think they need based on resource usage, security, and fault isolation. During performance testing, developers can adjust how components are allocated to packages if they find that process overhead or inter-process calls are a bottleneck.
Components that use the same resources, such as databases, should be grouped together in a single MTS server package. Remember that MTS manages resource pools on a per-process basis. Each MTS server package gets its own thread pool, database connection pool, Shared Property Manager (SPM), and so on. If two components using the same database are located in separate MTS server packages, they can not pool database connections. If the components are in the same MTS server package, they can pool connections. This capability can greatly improve developed application performance and scalability. Similarly, components in separate MTS server packages can not share properties by using the SPM. Components that need to share properties must be in the same package to ensure correct application operation.
The location of resources should also be considered when designing packages. In general, components should be deployed as close as possible to the resources they use, particularly data stores, to help reduce network traffic within applications. For example, if two data stores used by applications might be located on different computers, consider putting the data objects that use each data store in separate packages. This arrangement gives system administrators the flexibility to install each package near the data store it uses. Any deployment recommendations or requirements, such as locating a package and a data store on the same computer, should be noted in the physical design documentation distributed with each application.
Separating components into different packages will also ensure that a fault in one component does not cause other components to fail. MTS will terminate a process whenever it detects internal corruption. Exceptions within a component can also force an MTS server process to terminate. Any transient state maintained in running objects or the SPM will be lost. If an application has components that maintain transient state, consider placing those components in a separate server package. Components should also be isolated during quality-assurance testing.
MTS server packages are units of trust for MTS. Calls into a package can be secured. Calls within the package are trusted. Thus, application security requirements have a big impact on package design. If calls into a component must be authorized, clients and components must be allocated into different packages. Only components that can safely call each other without requiring an authorization check should be allocated into one package.
MTS security roles are defined on a per-package basis. If multiple components use the same roles, consider placing them in the same package. Generally, this method of grouping is safe, since components can be called by the same set of users, and should be trusted to call each other. Placing components that use the same roles in one package simplifies administration, since the system administrator does not need to remember to populate the role in multiple packages.
In addition, an MTS server package runs as a particular identity, and all components in the package run as the package identity. Components that need to run as different user identities should be in separate packages. Components might need to run as different identities because they should have different access rights to some resource or in order to maintain an audit trail. Although the exact identity each server package will run as cannot generally be defined during development, document any recommendations along with the permissions required by package components. For example, if a data object in a given package requires read/write access to a particular data store, document that fact.
Let's examine the general concepts behind components that will run within the MTS environment. The four major tasks of implementing such components are:
All components that run in the MTS environment must be implemented as in-process components. Each component must provide a class factory for each COM class in the component, as well as a type library that describes all COM classes and their interfaces.
In addition to these requirements, MTS components should be written as single-user components. The MTS Executive, mtxex.dll, provides thread management services, so developers don't need to cover multithreading issues in their components. In fact, threads should not be created on their own within their components. Components should simply be written as if one caller at a time will use them.
Recall that every in-process COM component has a ThreadingModel registry value associated with it; this registry value describes how the component's objects will be assigned to threads during execution. If a ThreadingModel value is not specified, the component uses the single-threaded apartment (STA) model. Thus, all objects will execute on the containing process main thread. COM synchronizes access to all objects on the main thread, so this approach is not very scalable. In addition, single-threaded stateful objects are prone to deadlock. However, if non-reentrant libraries are used within components, the STA model may need to be used.
A more suitable choice for MTS components is to use the apartment-threading model, by setting the registry key ThreadModel=Apartment. Remember that in the apartment-threading model, each object is assigned to a single thread for its entire lifetime, but multiple instances of the object can be created at once in any number of STAs. The component's global data and DLL entry points must be thread safe and protected from concurrent updates. This approach improves scalability without overly complicating their component implementation. It's not necessary to implement fully thread-safe components that support the free-threaded model; if these components are implemented, they must not create any threads on their own. Fortunately, every COM programming tool currently available can generate, at the push of a button, skeleton code that meets these requirements.
What about Java and COM? Java is a good language for implementing COM components. With the COM support provided by the Microsoft Virtual Machine for Java and Microsoft Visual J++, developers can easily create components from Java classes. MTS even provides a package to help developers access its features from Java: com.ms.mtx. The details of how developers code their components will vary. For additional information about writing components in Java, Paul Stafford's "Writing Microsoft Transaction Server Components in Java," available in the Microsoft Developer Network (MSDN) Library, is highly recommended.
The default behavior provided by COM programming tools is adequate for most components needed in a three-service layered application. If these defaults are used, developers can spend more time focusing on application-specific logic, and save hand-coding the basic COM component infrastructure for times when performance requirements cannot be met with the default code provided by their tools. Since COM is a binary standard, developers can change the implementation of any component without affecting its clients, as long as the public interface is not changed—even to the extent of changing implementation languages. In this book, we use the standard COM implementations provided by Microsoft Visual Studio tools. For information about hand-tuning components, or to learn more about how COM works, many books detailing the writing of COM components, especially for C++ developers, are available such as David Kryuglinski's Programming Visual C++, Fifth Edition and Guy and Henry Eddon's Inside Distributed COM (Microsoft Press, 1998).
Once basic in-process component framework is in place, COM class implementations can be added to the framework. Each COM class exposes its behavior through one or more COM interfaces. Some programming tools, such as Visual Basic, allow methods and properties to be exposed directly on the class and internally convert these methods and properties to a COM interface. Try to avoid this practice; always define COM interfaces explicitly by referring to the IDL. As mentioned, COM interfaces represent a contract between the caller and the object being called, and once published these interfaces must not be changed. When interface definition is mixed with implementation, changes could be made to interfaces without a full understanding of the possible consequences. Defining interfaces separately also helps when multiple developers are working on an application. Test objects that expose the interfaces can be created quickly to verify that interface-reliant client code works correctly.
In addition to the actual work of the method, each interface method implementation has two requirements. First, MTS must be notified when the object's state is finished. Second, errors must be handled within their objects, and MTS must be notified whether or not the method was successful. The Object Context associated with each object must be used. The IObjectContext SetComplete method indicates that an object has completed its work successfully. The IObjectContext SetAbort method indicates the object encountered an error.
When SetAbort is called, if the object is participating in a transaction, that transaction will fail and managed resources will be rolled back to their state prior to the transaction. This is an extraordinarily powerful mechanism for handling errors when many independently implemented objects are working together to perform some higher-level functionality requested by a client. If any one object can't complete its work, the entire request fails.
Since most objects will not retain state across method calls, the basic structure of each method will look something like the following pseudocode:
MyMethod(…) On Error Goto ErrorHandler CtxObject = GetObjectContext() Do work If error condition then Raise error CtxObject.SetComplete Return success ErrorHandler: CtxObject.SetAbort Return error
In addition to calling SetAbort when an error occurs, a normal COM error should be returned. Clients then have the information they need to decide whether to continue doing work. SetAbort does not have any effect until the containing transaction ends.
These basic ideas apply to both data objects and business objects. With the basic component framework and a skeleton implementation of each class in place, we can turn our attention to the interesting part: implementing the application logic.
Windows NT provides many of the services that were once the domain of the application developer, including:
In the following sections, we discuss Windows NT architecture, and how applications interact with each other to utilize these services.
Windows NT architecture is separated into a user mode and a kernel mode. Windows NT controls access to the memory on the system as well as access to all hardware. Applications running in user mode have no direct access to the hardware; they must communicate with the Windows NT Executive Service running in kernel mode to gain access to resources such as files, printers, communication ports, or even the processor.
Figure 8.4 shows Windows NT architecture and how applications behave and gain access to system resources.
Figure 8.4 Windows NT architecture
The Executive Service controls access to all resources. The I/O Manager is the heart of access to resources such as files, system devices, and the network. The Virtual Memory Manager is responsible for managing the computer's memory and setting up protected memory areas for each application running in user mode. The microkernel provides such basic services as interrupt handling and thread scheduling.
User mode contains applications and user processes. There is only one way for information to pass from user mode to kernel mode: through the Executive Service. This means that there is no other way for an application to bypass security on the Windows NT system or compromise the complete control that Windows NT has over the physical resources on the workstation.
User mode is composed of several subsystems where applications reside. These in turn send and receive messages from the Executive Service. The subsystems include OS/2 and POSIX, and the security subsystem.
In Figure 8.4, a Win32 application communicates with the proper subsystem through messaging to gain access to the required resources. The subsystem is responsible for passing requests to the Windows NT Executive to be fulfilled. The Executive Service first assures that the operation is safe (not something that would disrupt the system or violate system integrity) and then completes the operation.
The Windows NT Executive Service also manages memory. The Virtual Memory Manager provides applications with memory, and manages virtual memory and the Windows NT pagefile. Each application has its own dedicated memory address. Different operating systems allow applications to access different amounts of memory:
The virtual memory process, shown in Figure 8.5, works as follows:
Figure 8.5 Virtual memory process
Multitasking refers to the ability of multiple applications and processes to run at what appears to be the same time on a single system with a single processor. There are two types of multitasking systems: cooperative and pre-emptive.
The Windows 3.1 family of products is based on the concept of cooperative multitasking. The operating system grants each application access to the processor and system resources; it's up to that application to relinquish control of those resources for other applications to use. Clearly, a poorly written application that doesn't multitask well can easily bring the system to a halt, since that application is never forced to relinquish processor control.
The multitasking model used in the Windows 95, Windows 98, Windows NT and Windows 2000 families relies on pre-emptive multitasking. In a pre-emptively multitasked system, the system itself takes full control of the processor. In the case of Windows NT, the Executive Service controls the processor and resources at all times, not the applications. The Executive Service then grants access to the processor and other resources when necessary. Since the Windows NT Executive Service always controls hardware resources, such as the processor, it does not allow a single application or process to take control of the processor time and not relinquish it. For the application developer this means that it's no longer necessary to design an application to perform in a cooperatively multitasked environment, because this function is managed by the operating system.
In a multithreaded application, a single process or application can run several tasks at once. The Windows NT Executive Service grants each thread a slice of processor time. For example, a spreadsheet program may have an option that allows a recalculation to be done on a spreadsheet. By using multiple threads, the spreadsheet program can recalculate the spreadsheet in one thread and still allow the user to update other portions of the spreadsheet in another thread. Otherwise, to continue working, the user will have to wait until the recalculation is complete. Figure 8.6 illustrates both a single-threaded and a multithreaded process.
Figure 8.6 Multithreading in Windows NT