I l @ ve RuBoard |
|
When servers are busy, requests take a longer time to handle. Most of the time, the client simply waits longer. But sometimes, when servers are very busy, a request will simply time out, and an instance of RemoteException will be thrown. In this latter case, retry logic turns out to be fairly painful: if the server is too busy and cannot handle additional requests, the last thing in the world the client should do is send the request again, especially if the request isn't very important, or can wait awhile.
One way to deal with this is to use what I call the bouncer pattern . The idea is to define a new subclass of Exception , called ServerIsBusy , add it to all the remote methods, and then throw instances of ServerIsBusy whenever the server is too busy to handle additional requests.
In the simplest implementation, the server simply keeps track of the number of pending requests and throws an instance of ServerIsBusy whenever there are too many pending requests, as in the following implementation of the Bouncer class:
public class Bouncer { private static final int MAX_NUMBER_OF_REQUESTS = 73; private static in CURRENT_NUMBER_OF_REQUESTS; private static ServerIsBusy REUSABLE_EXCEPTION = new ServerIsBusy( ); public synchronized static void checkNumberOfRequestsLimit throws ServerIsBusy { if (MAX_NUMBER_OF_REQUESTS == CURRENT_NUMBER_OF_REQUESTS ) { throw REUSABLE_EXCEPTION; } CURRENT_NUMBER_OF_REQUESTS++; } public synchronized static void decrementNumberOfActiveRequests( ) { CURRENT_NUMBER_OF_REQUESTS--; } }
Once you've defined a bouncer class, you need to implement the check in all your remote methods. The code transformation is simple. A method such as:
public foo ( arguments ) throws exception-list { method body }
is rewritten as:
public foo( arguments ) throws exception-list, ServerIsBusy { Bouncer.checkNumberOfRequestsLimit( ); try { method body } finally { Bouncer.decrementNumberOfActiveRequests( ); } }
Adding this check to your server code has two main benefits. The first is that it enables the client application to distinguish between network failures and when the server is simply too busy. And the second is that it enables you to implement much friendlier client applications. In the simplest case, putting up a dialog box saying "The server is very busy right now, and as a result, this application won't perform very well" will save users a fair amount of frustration. More complicated clients might switch to a secondary server.
|
Suppose you're wrapping each remote method call in a retry loop, distinguishing the different types of remote exceptions, and stamping remote requests with identifiers. Then simple remote method invocations such as server.performAction( ) , in which server is a stub associated with some remote object, balloon to 20 or 30 lines of code, most of which simply deal with the complexities of failure handling. This is bad for two reasons. The first is that a simple and easy-to-read line of business logic has become cluttered with extraneous things. And the second is that a lot of code is being written over and over again (the failure-handling code is boilerplate code).
The solution to both of these problems is to encapsulate all the code you've been adding inside a single class. For example, you could define a new class called SpecificCallToServer which encapsulates all this code. And then server.performAction( ) becomes:
(new SpecificCallToServer( . . . ))..makeRemoteCall( )
This is a little less readable than the original code, but it's still very readable. And all the logic dealing with the network infrastructure has been neatly encapsulated into a single class, SpecificCallToServer . If SpecificCallToServer simply extends an abstract base class (named something like RemoteMethodCall ), you've made the client application more readable, and only written the code that deals with the complexities of making the remote method call once.
|
Wrapping remote calls in command objects also facilitates many of the other practices in this chapter. For example, using command objects makes it easier for the client to use a remote stub cache.
A naming service, such as the RMI registry or a JNDI service provider, provides a very simple piece of functionality: it lets a client application pass in a logical name (such as "BankAccountServer") and get back a stub to the requested server.
This level of indirection is incredibly useful. It makes writing the client code much simpler, it means that you don't have to figure out another way to get stubs to the servers (which isn't so hard: RemoteStub does implement Serializable ), and it allows you to easily move servers to different machines.
In short, using a naming service makes it much easier to write and deploy applications.
|
The javadocs for RemoteException say the following:
A RemoteException is the common superclass for a number of communication-related exceptions that may occur during the execution of a remote method call. Each method of a remote interface, an interface that extends java.rmi.Remote, must list RemoteException in its throws clause.
This might make it seem like it's OK, and maybe even a good thing, for your server-side code to throw instances of RemoteException . It's certainly easy, if you're working on a server and discover a new exceptional condition, to add a line of code such as the following:
throw new RemoteException("You can't deposit a negative amount of money");
It might even seem like good programming practiceafter all, the client code already catches RemoteException . But it's a very bad idea to use RemoteException in this way.
To understand why, you need to understand what RemoteException really means. The real meaning of RemoteException is that something has gone wrong between your client code and server code . That is, your client made a method call on a stub. Your server code is expecting to receive a method invocation via its skeleton. If something goes wrong between that call to the stub and the resulting invocation made by the skeleton, it will be signalled by an instance of RemoteException . Exceptions that happen within the server should be signalled by instances of some other exception class that doesn't extend RemoteException . There are two reasons for this. The practical one is that it's too easy for a client to misunderstand a RemoteException . For example, the retry loop shown earlier would try to invoke the remote method again. And the more abstract reason is that you should really be declaring the types of exceptions the server is throwing so that the client can react appropriately. Throwing generic exceptions is almost always a bad idea.
Almost all RMI exceptions extend RemoteException . This can be very convenient because it makes generic code easier to write; you can simply catch RemoteException and be confident that you've caught all RMI-related exceptions. But it can also lead to programmer sloppiness. The different subclasses of RemoteException have very different meanings, and treating them generically is often a mistake.
Every RemoteException can be classified using four major attributes: what code throws it, when it will be thrown, why it will be thrown, and what this indicates about your application. Consider the following list, which classifies the nine most common remote exceptions along these axes.
AccessException |
In the RMI infrastructure on the client side.
As part of the standard distributed communication between client and server, before the remote call is even attempted.
The client code doesn't have permission to open a socket to the server.
This indicates a client configuration problem (e.g., a problem in deploying the application). This exception is consistently thrown; it will not "spontaneously heal" if you try the call again.
AlreadyBoundException |
By the RMI registry.
Usually during launching, when the launch code attempts to bind the server into the registry.
The server code used bind( ) and the name was already taken.
This usually indicates a configuration error, that an instance of the server was already running, or a coding mistake (you meant to use rebind( ) ). In any of these cases, you probably need to clean out the registry.
ConnectException |
In the RMI infrastructure on the client side.
As part of the standard distributed communication between client and server. The client has tried to connect with the server but has been unable to establish a connection.
This is thrown by the RMI infrastructure on the client when a call fails. A ConnectException means the server object never got the method call at all.
If the network is working (e.g., if you can ping the server machine from the client machine), this exception usually means that the server isn't running (e.g., it crashed or was never started). Otherwise, it indicates a network failure.
MarshalException |
In the RMI infrastructure on the client side.
As part of the standard distributed communication between client and server. This is thrown after the client establishes a connection to the server (e.g., when a stream has already been created).
You tried to send an object that didn't implement either Serializable or Externalizable .
You must send an object that implements either Serializable or Externalizable .
NoSuchObjectException |
In the RMI infrastructure on the server side.
As part of the standard distributed communication between client and server. This is thrown after the RMI infrastructure on the server has already demarshalled the arguments and is trying to actually call a method on your code.
Every remote method call contains an ObjID , which uniquely identifies the object on which the method call is being made. The RMI infrastructure maintains a hashtable of instances of ObjID to RemoteServers . This error indicates that the ObjID passed over the wire was not a key in this table.
This usually occurs when a client has a stub to a server that no longer is running in the server process. This is a strange exception to encounter because stubs try to maintain leases on servers (which usually prevents a server from being shut down). As such, this exception usually indicates a failure in the distributed garbage collector (it doesn't indicate that the server process crashed; if the server process crashed, an instance of ConnectException would have been thrown instead).
NotBoundException |
By the RMI registry.
As part of a lookup( ) call.
The registry's hashtable of objects doesn't have any stub bound under the specified name.
This usually indicates that there is a transaction issue (a server was unbound while the client was interacting with the registry), or that the registry was restarted and not all the servers bound themselves in again.
StubNotFoundException |
In the RMI Infrastructure on the server side.
When an attempt is made to export an instance of RemoteServer that does not have an associated stub.
Stubs are necessary for RMI to function properly. The right point in time to signal this error is when the server is starting up (or just being exported), not when the stub is actually required.
This usually means you forgot to run rmic (or that you made a mistake when deploying your server).
UnknownHostException |
In the RMI Infrastructure on the client side.
When a particular stub is used for the first time.
The client is unable to resolve the server name using DNS.
The client and the server are on different subnets of your network, and somebody in IT configured DNS incorrectly.
UnmarshalException |
In the RMI infrastructure on the server side.
While attempting to unmarshal the arguments from a remote method call.
The server either cannot find the class referenced, or has an incompatible version of it.
The client and the server have different, and incompatible, versions of the codebase .
You shouldn't panic when you look at this list. It's not that complicated, and once you actually start thinking about the different types of RemoteExceptions , most of the information here will become second nature to you. The important point here is that these nine exceptions cover about 95% of the instances of RemoteException thrown in practice. And they are all thrown at different times, for very different reasons. If you write code that simply catches instances of RemoteException , you might be missing an opportunity to make your code more robust, better at reporting urgent problems to someone who can fix them, and more user-friendly.
|
The distributed garbage collector is a wonderful piece of code. It works in a very straightforward manner: a client gets a lease on a particular server object. The lease has a specific duration, and the client is responsible for renewing the lease before it expires. If the lease expires , and the client hasn't renewed the lease, the server JVM is allowed to garbage-collect the server object (as long as no other clients have leases against that particular object).
If a server implements the Unreferenced interfacewhich contains a single method, unreferenced( ) the server will be notified via a call to unreferenced ( ) that there are no valid leases against the server.
It's important to note that any active instance of a stub, in any JVM, will automatically try to connect to the server and maintain a lease. This means that, for example, if the server is bound into the RMI registry, the registry will keep the server alive . (The RMI registry basically stores the stub in a hashtable. The stub keeps renewing its lease.)
In turn , this means that if you're using a naming service to get instances of stubs, no other process can actually get a stub to a server if unreferenced has been called ( unreferenced will be called only if the server is no longer bound into any naming services).
All of this makes the unreferenced method an ideal place to release server-side resources and shut down the server object gracefully.
By default, a lease should last 10 minutes, and clients should renew every 5 minutes (clients attempt to renew when a lease is halfway expired ). The problem is that, in a wide variety of production scenarios, the default values don't work very well. Using JDK 1.3, I've experienced intermittent distributed garbage-collection failures (in which a client has a stub to a server that's been garbage-collected ) when the network is congested or starts losing packets.
Fortunately, you can change the duration of a lease by setting the value of java.rmi.dgc.leaseValue . This parameter, which is set on the server, specifies the duration of a typical lease. The trade-off is simple: smaller values for java.rmi.dgc.leaseValue mean shorter lease durations, and hence quicker notification when a server becomes unreferenced.
But smaller values also mean a greater chance of a false positive: if a client has trouble renewing a lease, giving the client a larger window in which to renew the lease (for example, before the client's lease is expired and unreferenced is called) is often helpful. In particular, larger values of java.rmi.dgc.leaseValue will make your system more robust when the network is flaky. I tend to use at least 30 minutes for java.rmi.dgc.leaseValue .
|
I l @ ve RuBoard |