Archived

This forum has been archived. Please start a new discussion on GitHub.

Ice Proxy Design

Hi,

In the process of coming up to speed on Ice (and some CORBA), I've run into a couple of issues about which I'm hoping to get a little clarification.

First, I was a little surprised that Ice (and apparently CORBA too) doesn't seem to apply the Proxy pattern as it's usually described (by the GoF, for example). Specifically, why doesn't the Slice compiler generate a common base from which the proxy and servant classes are both derived? I assumed (perhaps naively) that the ability to write client code which remains unaware of whether it's dispatching to a proxy or a real subject (i.e, servant) would be important.

Second, I was pleased to read early in the Ice Book that the Slice 'nonmutating' keyword facilitates use of the C++ 'const' keyword for the corresponding class member functions generated by the Slice compiler. But AFAICT, this translation is only applied for the servant class interface and not for the proxy. If I'm right about that, this means client-side APIs that depend on proxy interfaces must either avoid 'const' altogether or use const_cast everywhere. That seems like an unfortunate liability.

Thanks,
Greg

Comments

  • marc
    marc Florida
    For the first part of your question, please have a look at this thread:

    http://www.zeroc.com/vbulletin/showthread.php?t=1177

    The short answer is, in order to have a flexible distributed system, there can be no is-a relationship between a proxy and the servant. An invocation on a proxy is fundamentally different than an invocation on the servant itself. Trying to hide these fundamental differences destroys both location and implementation transparency.

    Note that older CORBA ORBs used an is-a relationship between proxy and servant, but because of the serious drawbacks of this design, newer CORBA standards explicitly mandated to not use this approach.

    As for the second part of your question, you cannot translate nonmutating into const proxy operations, because by invoking on a proxy, the state of the proxy can change (for example, it might establish a new connection), regardless of whether the operation is nonmutating or not.
  • I admit to being new to distributed computing and that there are many issues I'm unaware of, but it's not readily apparent to me how GoF-style proxies would destroy location or implementation transparency. Quite the opposite:
    client ---> interface
                 /       \
              proxy --> servant
    

    seems elegantly capable of meeting both of these requirements - after all, that's what it's for.

    The way it looks to me, Ice proxies aren't as effective as they could be when it comes to making location transparent precisely because they don't derive from the same abstract base as the servant. If location were truly transparent, much (most?) client code would be blissfully unaware of whether it's dispatching to a proxy or an actual servant. For that kind of code, whether the target object resides outside the client's address space should be an implementation detail that has nothing to do with its logical interface. In which case, if an operation is defined on an application interface as 'const', that operation should remain logically 'const' whether it's intercepted by a proxy or implemented by a servant.

    On a related note, the Ice Book often uses Coplien's "ambassador" terminology when describing the semantics of proxies. Note that, when Coplien says "the difference is transparent to the caller invoking the service" regardless of whether the caller is on the same processor as the "body", the transparency he refers to in his example can only realistically be achieved by deriving the proxy and servant bodies from a common interface.
  • marc
    marc Florida
    Please read the various posts that have already been written about this subject, and read the documentation about the relationship between Ice object, proxy, and servant. If this is too abstract, then please have a look at the documentation for servant locators, and also at the documentation for the Freeze evictor (which is one concrete implementation of a servant locator).

    You cannot have an is-a relationship between a proxy and a servant, because a proxy is-not a servant, and a servant is-not an Ice object. Instead, proxies allows you to invoke on Ice objects, and servants implement Ice objects.

    For example, if you invoke on a proxy, a servant might not even exist yet for the Ice object, but may be loaded on demand by a servant locator. In fact, several different Ice objects (with different proxies) might even use only one single servant. This is then called a default servant. Or there might be several servants for one Ice object to implement an Ice object redundantly. The point is, no matter how an Ice object is implemented, you must always be able to use a proxy to invoke on this Ice object, without having to be aware of the implementation details. That's called implementation transparency. And this must work regardless of whether the servant is collocated or not. That's called location transparency.

    It is important to understand that a proxy is a proxy for an Ice object, and not for a servant. As I wrote above, a servant is used to implement Ice objects, but it is not an Ice object. An Ice object is not a concrete language construct, but a concept for the development of scalable distributed applications. Therefore an Ice proxy is not the same as the GoF proxy.

    As an aside, you often cannot apply the GoF patterns 1:1 to distributed systems. For example, if you implement the Observer pattern exactly as described, you will run into deadlock problems. The last issue #4 of Connections has an article about this.
  • Marc wrote:
    Please read the various posts that have already been written about this subject, and read the documentation about the relationship between Ice object, proxy, and servant. If this is too abstract, then please have a look at the documentation for servant locators, and also at the documentation for the Freeze evictor (which is one concrete implementation of a servant locator).
    I've read the thread you directed me to and much of the Ice Book as well. I understand that an Ice proxy IS-A IceProxy::Ice::Object, that an Ice servant IS-A Ice::Object, that an Ice proxy object is a surrogate for an Ice servant object, and that Ice defines no IS-A relationship between proxies and servants. What I don't understand is why this is preferred over the GoF approach to proxies when it can accomplish the same thing AFAICT. I haven't read the servant locator and Freeze documentation yet though, so I'll look into it.

    Marc wrote:
    You cannot have an is-a relationship between a proxy and a servant, because a proxy is-not a servant.
    I apologize for the poor class diagram, because that's not what I intended to show. My intent was: clients use interfaces which can be implemented either as proxies or servants, and proxies use servants to do their work.

    Marc wrote:
    For example, if you invoke on a proxy, a servant might not even exist yet for an Ice object, but may be loaded on demand by a servant locator. In fact, several different Ice objects (with different proxies) might even use only one single servant. This is then called a default servant. Or there might be several servants for one Ice object to implement an Ice object redundantly.
    Understood. But a GoF-style proxy affords you the same flexibility.

    Marc wrote:
    The point is, no matter how an Ice object is implemented, you must always be able to use a proxy to invoke on this Ice object, without having to be aware of the implementation details.
    That depends upon the application, and this is were my concern lies. I agree that a proxy is required if an object implementation needs to be remoted. But if I (or my application) am smart enough to know when one or more objects will be collocated, accessing them through proxies is unnecessary. That's the reason the GoF-style proxy is attractive: much (most?) client code will continue to work without a re-compile either way.

    Marc wrote:
    It is important to understand that a proxy is a proxy for an Ice object, and not for a servant. As I wrote above, a servant is used to implement Ice objects, but it is not an Ice object.
    This makes me wonder whether we understand each other, because the server-side Slice-to-C++ language mapping translates a Slice interface into a C++ abstract class which is publicly derived from Ice::Object. That's an IS-A relationship in my book.

    I'm (i.e., the company I work for) in the early stages of developing a distributed system, so I'm trying to understand how Ice (and CORBA for that matter) works and whether it will suit our needs. The Slice-to-C++ standard library binding, exception inheritance, UDP support, threading constructs, IceStorm and Freeze are all big wins, but the fact that I can't swap proxies for servants GoF-style when I know a priori that it's desirable to do so seems unfortunate. Perhaps I could build wrappers to do this instead.

    -Greg
  • marc
    marc Florida
    Either myself or someone else from ZeroC will answer your other questions later, however, let me just comment on this one:
    jghickman wrote:
    This makes me wonder whether we understand each other, because the server-side Slice-to-C++ language mapping translates a Slice interface into a C++ abstract class which is publicly derived from Ice::Object. That's an IS-A relationship in my book.

    Unfortunately, the class name Ice::Object is bit confusing. Ice::Object is the base class for servants. Ice objects, on the other hand, are a concept, not programming language objects. Servants implement Ice objects, and proxies invoke on Ice objects.

    For example, you can have millions of Ice objects in your application, but only very few that have an actual servant in your server process. The rest might have their state in a database (such as Freeze), and when a proxy invokes on them, a servant is instantiated and the persistent state is loaded from the database into the servant. Later, when the Ice object is not needed anymore, the modified state is stored again in the database, and the servant is discarded.

    This is just one possible method to achieve virtually unlimited scalability. There are many others, such as having only one single servant for all these millions of Ice objects, and implementing each servant method as a database lookup for the Ice object.
  • Hi Greg,

    pretty much all the pre-POA CORBA ORBs (Visibroker, Orbix, Orbacus 1, etc) used the same hierarchy for the client and server side. With the POA, the client- and server-side APIs were split because not doing so caused problems. Ice follows the same philosophy. Here are some of the reasons:
    • The life cycles of proxies, servants, and Ice objects are orthogonal and independent. For example, I can create a proxy in isolation, without having a servant, and I can have an Ice object (which is a concept, not a real programming-language artifact) without having a servant or a proxy. This separation of the conceptual Ice object from its client- and server-side manifestation is important, especially when it comes to scalability: by separating the concepts, I can, for example, take advantage of lazy initialization, by creating proxies without also having to create servants and using a servant locator to instantiate servants on demand.
    • Providing a separate hierarchy for proxies and servants and invoking via the proxy allows me to change between collocated and non-collocated servants without breaking any code. (In other words, I can freely move servants in- and out-of-process.) Without such a separation, this becomes impossible. Note that the current design does not prevent you from invoking directly on a servant, if client and server are collocated (but calling directly into the servant has to be a concious decision because a proxy is-not-a servant). So, if you want to break location transparency, you are free to do so; it's just that it has to be a deliberate action.
    • Any direct invocation on a servant completely by-passes the Ice run time. If proxies were to be the same as a servant, this would make it impossible to use, for example, servant locators in the collocated case. Similar arguments apply to other kinds of run-time intervention (such as, for example, transparent propagation of a transaction context or similar information).
    • With Ice, you can activate a servant and have it disappear automatically as soon as the final invocation drains out of a servant by simply deactivating the servant. This feature is essential for proper life cycle management. If proxies were the same as servants in the collocated case, it would become impossible to such a reference-counting strategy: either the existence of a proxy would prevent the destruction of the servant, or the destruction of the servant would result in a dangling proxies.
    • If proxies and servants were the same, implementation techniques such as default servants would become impossible because, in the absence of a separate proxy, there would be no way to implicitly supply object identity to a servant. In other words, if I want to use default servants (or any other many-to-one mapping of Ice objects onto servants), the identity of the Ice object cannot be implicit in the identity of the servant, meaning that I must have a separate proxy. This is particularly important for scalable servers that act as a front end to some existing system, such as database records. For the system to scale and be robust, servants must be stateless, and if servants have implicit object identity, they are no longer stateless (because object identity is state).
    Note that, when Coplien says "the difference is transparent to the caller invoking the service" regardless of whether the caller is on the same processor as the "body", the transparency he refers to in his example can only realistically be achieved by deriving the proxy and servant bodies from a common interface.

    This does not really apply (and Jim and I have had discussions about this in the past--Jim agrees with me on that point). The conceptual picture when programming with Ice (or CORBA, for that matter) is not that a proxy is-a servant. Instead, you need to think of a proxy as a pointer to an Ice object (which is not the same as an Ice object, a servant, or a pointer to a servant).

    Whether a servant is collocated or not, by always invoking on a proxy, the same single programming model applies regardless of location: I invoke on the proxy, which somehow sends the invocation to the Ice object. If the Ice object does not exist, I get an ObjectNotExistException (instead of undefined behavior); if the Ice object exists, the invocation succeeds. If the Ice object cannot be reached, I get a ConnectionRefusedException or similar.

    Note that all of this is completely independent of whether a servant exists or not. In fact, the Ice object model has no notion of a servant at all: the object model only understands proxies and Ice objects. This is evidenced by the Slice type system: it is impossible to talk about servants in Slice. All you can talk about are proxies and interfaces; instances of interfaces are Ice objects, and proxies are pointers to these Ice objects.

    A servant is a server-side implementation artifact and the concept of "servant" simply has no meaning for the client side. In order to preserve the separation of interface and implementation, it follows that there must be no way for client-side code to have any notion of a servant, because servants simply do not exist in the client-side programming model. But, by deriving the proxy and the servant from a common base, we would allow the server-side implementation artifact to spill over into the client-side programming model and lose this separation.

    Cheers,

    Michi.
  • Guys,

    Thanks for creating and helping me understand Ice.

    Greg