Sie sind auf Seite 1von 5

Design and Implementation Considerations in a Distributed Observer System

Karl Ridgeway

David Bernstein

John Magnotti

James Madison University Dept. of Computer Science Harrisonburg, VA

James Madison University Dept. of Computer Science Harrisonburg, VA

James Madison University Dept. of Psychology Harrisonburg, VA

karl.ridgeway@gmail.com ABSTRACT

bernstdh@jmu.edu

john.magnotti@gmail.com

The observer pattern is widely used to decouple objects in a publish/subscribe relationship. Because many distributed systems contain such relationships, this pattern is a natural expression of these systems. However, implementing a distributed observer system places unique constraints on both the implementation of the pattern and the remote procedure call framework that is used to support it. This paper explores these constraints and discusses some idioms to work around them while maintaining the semantics of the original pattern.

use-cases for the observer pattern. The MVC architecture is composed of three basic elements: Model The data being presented View The presentation of the model to the user Controller The translation of user commands into actions on the model Since the primary purpose of MVC is to reduce coupling between these three components, this architecture is widely employed when there is a need to locate one or several of these on disparate physical machines. Since most of the inter-object messages in an MVC system are notications of changes in objects state (e.g. The model notifying the view of a change in the data), the observer pattern is often the mechanism used to connect these components. Therefore, to ensure the correctness of a distributed MVC system, it is essential to examine the implementation of the underlying distributed observer system. This paper discusses the distributed observer pattern in the context of the four phases of the observer lifecycle: bootstrapping/discovery, normal execution, handling failure cases, and gracefully shutting down the system. During each phase, we will discuss the additional constraints placed on both the pattern and on the remote procedure call (RPC) mechanism.

Categories and Subject Descriptors


D.2.8 [Software Engineering]: Designmethodologies

General Terms
Design

Keywords
Obsever Pattern, Proxy Pattern, RPC

1.

INTRODUCTION

The observer pattern as described in [2] is one of the simplest, most powerful, and most frequently employed patterns in the pattern literature. Its ability to decouple a subject from its observer and to broadcast state changes transparently makes it an essential component pattern of many common higher-level architectures. The model/view/controller (MVC) architecture is widely considered one of the seminal Use with permission only. Current aliation: Rosetta Stone, Inc. Corresponding author. Current aliation: Auburn University

2. THE THREADING ENVIRONMENT


As discussed in [1], the most signicant dierence between an application running on a single computer and a distributed application is that the distributed application is by nature executed in multiple threads, at least one per computer. While it is possible to implement an observer-like system in which multiple threads are executing simultaneously, the observer pattern is normally described in the context of a single thread of execution. Therefore, this paper only discusses systems composed of a single logical thread. Specically, we assume that we are using an RPC framework that supports synchronous, blocking method invocations (e.g. Suns RMI) to guarantee that only one computer may be executing code that is part of the pattern at a time. Note that this assumption does not preclude the development of a hybrid multiprogrammed and distributed application (see [1], for example). Instead, this assumption places a limit on the observer system in that application.

Technical Report IVSL-2 (April 2007)


Immersive Visualization Systems Lab James Madison University Harrisonburg, VA 22801

3.

BOOTSTRAPPING THE OBSERVER

In order to initialize a group of objects in a pattern, the participating objects need to be constructed and related to one another. For obvious reasons, we refer to this as bootstrapping. In the case of the observer pattern, the bootstrapping process can be described as follows: 1. A subject is created; 2. One or more observers is created; 3. The observer obtains (or is given) a reference to the subject; and 4. The subject is given a reference to the observer (or the observer registers itself with the subject). We now discuss these steps in the context of a system that includes distributed observers.

services in a remote method call (see [3]). As another example, Javas RMI can be extended to pass a direct reference to the remotely accessible object by copying the RMI stubs (as in JNDI).

3.3 Registering with the Subject


In order to nish the bootstrapping process, an observer must be registered with the subject. Typically, a message containing a reference to the observer is sent to the subject. Hence, we again need a mechanism for passing references in an RPC framework. Obviously, we could use the mechanism discussed above to pass references. However, since the observers are typically running in separate threads (indeed on dierent machines), these registrations are not naturally atomic (relative to one another and/or update notications). Given our assumption that the RPC framework connes the participants in the (distributed) observer pattern to a single logical thread, this natural concurrency does not cause any problems.

3.1 Creating the Participants


Because the creation of a subject and observers is generally done dynamically during the execution of a program (i.e., new observers can be constructed and registered at runtime), the RPC framework employed must support the dynamic promotion of an Object from local to global accessibility. Fortunately, most systems we encountered (RMI, CORBA, DCOM, .NET Remoting, etc...) do provide support for this feature.

4. NORMAL OPERATION
The two major additional constraints that distributing an observer implementation places on the design of the observer and on the RPC framework involve enforcing the implicit contracts of the Observer and optimizing implementation details for performance. In order to enforce the operational semantics and preserve transparency of the pattern, it is essential to examine its dynamic behavior. In general, any collaboration of the subject/observers involves: 1. Changing the subjects state through an invocation of setState(). 2. Invoking the subjects notify() method (which causes the subject to iteratively invoke the update() method in each of its observers). 3. Each observer responding by either ignoring the update, querying the subject for the changed state, or using the information passed as an argument to its update() method.

3.2 Obtaining a Reference to the Subject


After creating the subject and observers, the program should provide some way for the observers to register themselves with the subject. To accomplish this, the observer needs to obtain a reference to the subject. In general, there are two approaches: Discovery (Observer-Pull) The observer uses a remote object discovery mechanism to locate the subject and obtain a reference to it (or another object discovers the subject and injects that reference into the observer). Smart References (Subject-Push) The subject initiates a remote method call that sends a back-reference to the machine running the observer. Distributed systems commonly use a discovery mechanism (e.g., Suns JNDI) to obtain references, even though it does make the code more complex and introduces additional participants. The shortcoming of this approach, especially in the context of distributed observers, is that it signicantly dierentiates the distributed and non-distributed implementations. That is, non-distributed systems that use the observer pattern rarely use a discovery mechanism. Hence, in order to maintain the semantics of the classical pattern, we often advocate the use of smart references. Not surprisingly, most Java-based RPC frameworks do not support arbitrary reference-passing across remote method calls. This is due to the fact that Javas pass-by-value semantic breaks down when passing a reference to an object (its address in memory) out of its original address space. However, there are approaches that make this possible. For example, Moxaic has a command pattern-based RPC mechanism that provides a facility for transporting references to

4.1 Enforcing the Observer Contracts


There are several constraints, some explicit and some implicit, that make up the set of contracts for the observer pattern. Probably the most important of these is the assumption that no observer may change the subjects state during an update notication, thus triggering another (potentially recursive) round of update notications. In practice, this contract can be strengthened by employing the use of the push model (see [2]) to supply observers with the information they need to react to a subject state change. By not providing the observers with a back-reference to the changed subject, we can prevent them from triggering another state change and breaking this contract. However, since each remote method invocation is potentially expensive in a distributed program, this push model is less attractive.1
1 The use of a true broadcast mechanism will be discussed shortly.

Another implicit contract of the observer pattern is that only the subjects notify() method may call update() on an observer. This prevents potentially expensive computation when the subject has not changed. Unfortunately, this contract is frequently broken. For example, consider the following common Java Swing idiom:
c l a s s MyButton extends J B utton implements { p u b l i c MyButton ( ) { addActionListener ( this ) ; } ActionListener

4.2 Performance Optimization Considerations


Performance and eciency are always among the most important concerns when distributing an application. Any problems caused by excessive or redundant method invocations are exacerbated by the relatively slow communications channel. Perhaps the easiest performance optimization discussed in [2] involves moving the responsibility of calling notify() on the subject from the subjects mutator methods to the client. This approach prevents unnecessary calls to update() by providing the client with a way of mutating the subject atomically, without updates triggered on every mutator method. Unfortunately, this places the burden of remembering to call notify() on client code - an approach that encourages errors, since clients may neglect to call notify(). One way to get the best of both worlds is to limit access to the mutator methods and force clients to create Command objects to modify the subjects state. For example consider the following Subject and ConcreteSubject:
/ A S u b j e c t has 3 s t a t e f u l f i e l d s : s t a t e 1 , state3 . / public i n t e r f a c e S u b j e c t { p u b l i c i n t e r f a c e M u t a b l e S u b j e c t extends { void s e t S t a t e 1 ( O b j e c t n e w S t a t e ) ; void s e t S t a t e 2 ( O b j e c t n e w S t a t e ) ; void s e t S t a t e 3 ( O b j e c t n e w S t a t e ) ; } state2 , and

p u b l i c void a c t i o n P e r f o r m e d ( A c t i o n E v e n t { doCoolStuff () ; } private { } } void doCoolStuff ()

e)

J B utton

b u t t o n = new MyButton ( ) ; cool stuff before showing the button !

// Do some

button . acti onPerfor m ed ( n u l l ) ;

In this example, the author intended to programatically trigger the doCoolStuff() method instead of waiting for a user to click the button. That is, the actionPerformed() method [which plays the role of the update() method] is being called outside of the subjects notify() method. One way to enforce this contract is to make the update() method visible to only the subject (e.g., using package visibility). This is not particularly appealing, however, because it forces all observers to have a special relationship (e.g., being in the same package) with the subject that is not normally included in the observer pattern. Another way to enforce this contract is to use authentication (e.g., using public key encryption). That is, include a parameter in the update() method that can be used to authenticate the caller. This is also not particularly appealing because it adds complexity and imposes on the design. A third way to enforce this contract is to have the observer create an inner class that implements the Observer interface and calls the real implementation of the method. For example, if we refactor the above example using this idiom we get:
c l a s s MyButton extends J B utton { p r i v a t e A c t i o n L i s t e n e r b u t t o n L i s t e n e r = new ActionListener () { p u b l i c void a c t i o n P e r f o r m e d ( A c t i o n E v e n t e ) { doCoolStuff () ; } }; p u b l i c MyButton ( ) { addActionListener ( buttonListener ) ; } private { } } void doCoolStuff ()

Subject

p u b l i c i n t e r f a c e SubjectMutatorCommand { p u b l i c void m utate ( M u t a b l e S u b j e c t s u b j e c t ) ; } Object Object Object getState1 () ; getState2 () ; getState3 () ; cmd ) ;

void m utate ( SubjectMutatorC ommand }

p u b l i c c l a s s C o n c r e t e S u b j e c t implements M u t a b l e S u b j e c t { private Object s t a t e 1 , s t a t e 2 , s t a t e 3 ; public Object g e t S t a t e 1 ( ) { return s t a t e 1 ; } public Object g e t S t a t e 2 ( ) { return s t a t e 2 ; } public Object g e t S t a t e 3 ( ) { return s t a t e 3 ; } p u b l i c void s e t S t a t e 1 ( O b j e c t { t h i s . s t a t e 1 = newState ; } p u b l i c void s e t S t a t e 2 ( O b j e c t { t h i s . s t a t e 2 = newState ; } p u b l i c void s e t S t a t e 3 ( O b j e c t { t h i s . s t a t e 3 = newState ; } newState )

newState )

newState )

This technique is attractive because it doesnt lower the readability of the code, it increases the strength of the observer contract, and we no longer have a subject and observer implemented in the same class (even though the subject is still technically reacting to itself). Unfortunately, this technique is rendered useless in a distributed situation because most RPC frameworks (including RMI and the Moxaic framework) are unable to gracefully serialize the synthetic eld representing the inner classs reference to its parent.

p u b l i c void m utate ( SubjectMutatorCommand { cmd . m utate ( t h i s ) ; notifyObservers () ; } p r i v a t e void { // . . . } } notifyObservers ()

cmd )

and the following use of them:

Subject

s u b j = new C o n c r e t e S u b j e c t ( ) ;

failure cases: An exception thrown by the subject; and An exception thrown by the observer. We break it down in this manner because code executing in these two objects will, in a distributed observer system, be executing on separate machines. As a result, distributed observer systems will exhibit dierent failure patterns from non-distributed systems. On a single machine, both of these scenarios will result in an exception bubbling up to the root of the call stack and either being handled or causing the application to exit. A failure in the subject code on a remote machine will be a silent failure by default. In eect, the observers simply stop receiving update notications. Hence, there is no semantic dierence between the subject shutting down and failing remotely. There is no chance for an exception to bubble up to the observers because they are not present in the logical call stack (having initiated, directly or indirectly, an active call through the failed subject). While the RPC mechanism could very well detect a remote subject failure, adding a failure message dramatically changes the pattern and creates a serious burden for programmers using the system. Alternatively, one could construct a polling system to routinely check the subject for liveness. However, this is well outside the domain of the traditional observer pattern implementation, and would require a serious amount of additional code and decidedly inelegant modications to the original system. When a thread fails while running inside an observer, most RPC mechanisms will report the exception back to the subject (since it is part of the logical call stack). At this point, the exception can be handled gracefully, the observer removed from the list, and the application can continue running. Since observers are on separate machines, this situation is slightly preferable to the single-machine model. That is, observer machines can go down unexpectedly, but the application can continue running. To shut down a system using the observer pattern, it is necessary to either de-register all observers, or simply delete the subject through some special shutdown mechanism. Deregistration is similar to the above section on observer registration. Deleting a subject seems to be similar to the traditional implementation. Since a subject can be removed silently, the only concern here is to inform the observers of the deleted subject (which can be done through a notify() call (see pg. 297 in [2]).

/ By p l a c i n g a l l t h r e e s e t S t a t e ( ) c a l l s i n s i d e one SubjectMutatorCommand , we a v o i d c a u s i n g excessive update n o t i f i c a t i o n s . / s u b j . m utate ( new SubjectMutatorCommand ( ) { p u b l i c void m utate ( M u t a b l e S u b j e c t s u b j ) { s u b j . s e t S t a t e 1 ( new O b j e c t ( ) ) ; s u b j . s e t S t a t e 2 ( new O b j e c t ( ) ) ; s u b j . s e t S t a t e 3 ( new O b j e c t ( ) ) ; } }) ;

In this example, access to the mutator methods in ConcreteSubject is limited by the convention of always using the Subject interface type instead of MutableSubject or ConcreteSubject. This approach is advantageous because it preserves the semantics of placing the notifyObservers() call inside the subject, and enables client code to create a transaction out of multiple mutations (calls to setState*()). This technique avoids excessive calls to notifyObservers() which have the potential to be very expensive in the context of a remote observer implementation. However, it is important to note that this solution is specic to the pull model. That is, it assumes that observers will call back to the subject to obtain further information about the state change. This system could be extended to support a push model by logging calls to each mutator method and using that information to provide an aggregate list of state changes that occurred. However, this variation of the push model assumes that observers only care about the modications made since the last notify(). The possibility of using a true broadcast communications mechanism to support ecient notify() calls is mentioned in [2]. Since there usually exists a one-to-many relationship between subject and observer (and an RPC mechanism makes remote calls expensive), this option seems attractive at rst glance. Unfortunately, there are some serious drawbacks to this approach. Most importantly, any such system would have to be painstakingly written by hand, since most RPC frameworks (e.g., RMI, CORBA, Moxaic) do not support broadcast communication. RPC frameworks do not support broadcast communication because this style of message is fundamentally dierent from a procedure call. A broadcast message naturally executes concurrently for multiple receivers and it generally receives no return value (and cannot communicate error conditions through exceptions). In fact, the concurent nature of the broadcast message breaks our single-logical-thread rule by causing truly concurrent calls to update(). While such a system may in fact prove very ecient, it breaks the Observer contract by not being a true procedure call and reduces application code transparency by circumventing the RPC framework. Another common performance optimization involves creating a return type for the update() method (often a boolean value). This technique is usually used one of two ways: to handle an event notication (for example, in an event bubbling scenario), or to unregister an observer after this call to notify(). Adding a return type to the update method places the additional burden of supporting return values on the RPC framework.

6. CONCLUSION
This paper explored some of the implications of employing the use of an RPC framework when developing a distributed implementation of the observer pattern. We considered its impact on the design of the pattern and the requirements it places on the RPC framework through the various stages of the observer lifecycle. Several considerations arise during the bootstrapping/initialisation phase. To facilitate the dynamic nature of observer registration, the RPC framework must support dynamic promotion from local to global accessibility. Luckily, this seems to be widely supported. We also discussed two possible strategies for providing a subject reference to the

5.

HANDLING FAILURE CASES AND SHUTDOWN


In this section, we will examine the behavior of two general

observers (and for providing observer references to the subject during registration). An active discovery mechanism such as JNDI is easily implemented and places no additional constraints on the RPC framework, but somewhat changes the reference-passing semantic of the pattern. Alternatively, the use of a passive reference-injecting mechanism in which the subject pushes a back-reference to itself (indirectly) to a remote observer implies that the RPC mechanism has a facility for transporting remote references. When considering the operational phase of the Observer in a distributed environment, we examined some additional diculties that the RPC framework imposes on the enforcement of the Observer contracts. One such contract species that no observer may change the subjects state during a notication. A natural method of strengthening this contract is to employ the use of the push model mentioned in [2]. Unfortunately, this option becomes less attractive in a distributed situation due to the expense of sending additional information in a remote procedure call. A common violation of the observer contract is the misuse of the update() method on the observer by a participant that is not the subject. We discussed some potential methods to strengthen this contract, including package visibility/friend classes, an authentication system, and the use of a Command to trigger the update. Unfortunately, the most attractive option, the Command, is not usually feasible in a distributed environment. The additional cost of method invocations places significant constraints on the design of a distributed Observer system. We therefore examined several design aspects of the Observer that can be optimized to reduce the number of method invocations. To avoid additional redundant invocations of the update() method on observers, we can employ a transactional strategy that can be adapted to work in a distributed environment by employing the Command pattern. Another common idiom for reducing the volume of subject/observer message passing is to make use of a return value for the update() method. This can become an issue if the RPC mechanism does not support return types. Finally, we considered employing a true broadcast communication mechanism to facilitate observer updates. Unfortunately, a broadcast message is functionally too dierent from a traditional message to be considered a functionally equivalent replacement. Finally, we considered the behavior of the distributed observer during the termination phases. These included failure cases and graceful shutdown. We noted that a subject failure on a remote machine will fail silently, unlike a subject failure on a single-threaded application. A remote observer will also fail silently. However, this could potentially be an improvement on the single-threaded model. Fortunately, gracefully shutting down the system poses no additional concerns beyond those already covered.

[3] K. Ridgeway, D. Bernstein, and J. Magnotti. Design and implementation of a remote procedure call framework. Technical Report 3, Immersive Visualization Systems Lab, James Madison University, 2007.

7.

REFERENCES

[1] G. R. Andrews and F. B. Schneider. Concepts and notations for concurrent programming. Computing Surveys, 15(1), 1983. [2] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns:Elements of Reusable Object-Oriented Software. Addison-Wesley Publishing Company, Reading, Massachusetts, 1995.

Das könnte Ihnen auch gefallen