Ice Archive - Matthew Newhook

Integrating Ice with a GUI revisited

July 28th, 2008 | Filed under Ice | No comments

I wrote a series of Connections articles in 2006 that explored issues with using Ice in a graphical application. The first three articles concentrated on the GUI event loop, and specifically on strategies for issuing remote invocations without adversely affecting the user experience. The central problem was that all remote invocations had the potential to block, even if they were oneway or asynchronous.

In my first article, which appeared in Issue 12, I introduced a call queue to avoid blocking the GUI thread. The idea was to write a class for each remote operation that the application needed to invoke; adding an instance of this class to the queue scheduled an invocation by a separate thread. The following code demonstrates the API:

class Call : public Shared
{
public:
    virtual void execute() = 0;
};
typedef Handle<Call> CallPtr;

class SayHelloCall : public Call
{
public:
    SayHelloCall(const HelloPrx& hello) :
        _hello(hello)
    {
    }

    void execute()
    {
        _hello->sayHello();
    }

private:
    const HelloPrx _hello;
};

CallQueuePtr queue = ...;
HelloPrx hello = ...;
queue->call(new SayHelloCall(hello));

The call queue implementation was relatively simple: a dedicated thread executed the queued calls in order, essentially serializing all of the invocations. A side-effect of this implementation is that it guaranteed strict ordering of twoway requests in the same server.

I enhanced the call queue in Issue 13 to support multiple concurrent requests using two different implementations: one using asynchronous invocations and the other using a thread pool. Both strategies sacrificed the strict ordering guarantee in order to improve throughput. I also introduced the idea of using multiple call queues for messages with different qualities of service (such as oneway, twoway, and batched messages).

I presented an advanced technique in Issue 14 that used the Ice router facility to eliminate the need to define a Call class for each remote operation. With this technique, invocations on Ice objects from the GUI are accomplished using the standard asynchronous API. The resulting code is considerably more straightforward:

class AMI_Hello_sayHelloI : public AMI_Hello_sayHello
{
public:
    void ice_response()
    {
    }

    void ice_exception(const Ice::LocalException&)
    {
    }
};

HelloPrx twoway = // Some twoway proxy.
twoway->sayHello_async(new AMI_Hello_sayHelloI());

The disadvantage of this technique was that, along with the complexity of having to implement an Ice router, oneway invocations were not handled gracefully. Instead, they required the use of a special request context key, similar to the one used by the Glacier2 router.

We were not very happy with this situation, and we went about fixing it in the Ice 3.3 release. To ensure that asynchronous calls never block, Ice 3.3 introduced some major changes to the internal architecture, which Benoit and Mark detailed in their article “Background I/O” in Issue 28 of Connections. The Ice run time now maintains its own queue of outstanding calls. Unlike my call queue implementations, Ice’s queue is hidden from the application and much more efficient because calls only need be queued if they cannot be sent immediately.

Contrary to what you might expect, however, synchronous oneway invocations such as the one shown below may still block:

HelloPrx twoway = // Some twoway proxy.
HelloPrx oneway = HelloPrx::uncheckedCast(twoway->ice_oneway());
oneway->sayHello();

The call to sayHello can block the calling thread during connection establishment, while performing DNS lookups, or because the server is slow or non-responsive.

Why did we decide to preserve the existing semantics of synchronous oneways? It certainly would be possible to make oneway invocations non-blocking, but that would introduce a big problem: how would we report errors back to the application? What should Ice do if, for example, a DNS lookup failed? Since the call to sayHello would not be allowed to block, the DNS lookup would have to occur in a separate thread, so the call to sayHello could return. But once the call to sayHello has returned, there is no obvious way to report the error to the application.

Ice 3.3 solved this problem in a different way by adding support for asynchronous oneways. These work much like asynchronous twoways in that you have to define an AMI callback object. The primary difference is that ice_response is never called because oneway invocations do not have responses. If an error occurs while Ice attempts to send the message, the Ice run time invokes ice_exception on the callback as usual. The code below shows how to make an asynchronous oneway request:

class AMI_Hello_sayHelloI : public AMI_Hello_sayHello
{
public:
    void ice_response()
    {
        assert false;
    }

    void ice_exception(const Ice::LocalException&)
    {
    }
};

HelloPrx twoway = // Some twoway proxy.
HelloPrx oneway = HelloPrx::uncheckedCast(twoway->ice_oneway());
oneway->sayHello_async(new AMI_Hello_sayHelloI());

One of the first applications to take advantage of the new non-blocking oneway semantics was IceStorm. Much effort went into prior versions of IceStorm to ensure that invocations from publishers would never block, a non-trivial task given that there was no simple way to make non-blocking invocations. With the new capabilities in Ice 3.3, I immediately re-architected and simplified the IceStorm internals to take advantage of the new non-blocking semantics. However, once I started performance and stress testing, I noticed that there was a new issue: there was no flow control on the non-blocking invocations. The upshot of this was that flooding IceStorm with events caused the memory usage to climb very quickly. To solve this problem we added a callback that Ice invokes after a queued request is actually sent. This can be used to flow-control oneway events.

To receive this notification, an AMI callback object must implement the Ice::AMISentCallback interface. Calling the asynchronous oneway method returns false if the message was queued, or true if the message was sent immediately. When the queued message is eventually sent, the Ice run time invokes the AMISentCallback::ice_sent method on the callback object:

class AMI_Hello_sayHelloI : public AMI_Hello_sayHello, public Ice::AMISentCallback
{
public:
    void ice_sent()
    {
        /* called when the message is sent*/
    }

    // ice_response, ice_exception implementation here
};

HelloPrx twoway = // Some twoway proxy.
HelloPrx oneway = HelloPrx::uncheckedCast(twoway->ice_oneway());

if(!oneway->sayHello_async(new AMI_Hello_sayHelloI()))
{
    // The request was queued, and ice_sent will be called
    // once the message is sent.
}

The addition of the guaranteed non-blocking semantics renders much of the content of my first three articles obsolete. Developing GUI applications with Ice is now much more straightforward, and I can offer much simpler advice: all remote invocations made from the GUI should be asynchronous!

Earlier I mentioned the issue of strict ordering guarantees for twoway invocations. The call queue implementation in Issue 12 preserved the order of requests, whereas the subsequent strategies did not (and neither does the non-blocking asynchronous solution in Ice 3.3). If your application depends on the order in which requests are dispatched in the server, you must take additional measures such as disabling portions of the UI while a call is in progress.

Finally, since AMI callbacks are invoked by an Ice thread, it may not be safe for your callbacks to manipulate the GUI directly. This is the topic of my article in Issue 15, which is still as relevant as ever.

Google Protocol Buffers Integration

July 19th, 2008 | Filed under Google protocol buffers, Ice | 4 comments

I was very interested last week to see the release of Google Protocol Buffers. Any contribution to the open source community should be congratulated!

The rapid demise of XML as a data store for large amounts of non-human readable structured data, and for RPC (such as SOAP), comes as no surprise to me. XML, although touted as such, is clearly not human readable. On top of that it is both bandwidth and storage intensive, and requires huge amounts of CPU cycles to process. Even in this day and age of fast CPUs, terabytes of storage and gigabit networks, every byte and cycle still counts, especially for huge companies like Google with vast amounts of data. Bandwidth is not free, and neither are cycles (someone has to pay that power bill!).

Now we’ve reached a new milestone. Web services are quickly dying a well-deserved death. WSDL, despite arguments to the contrary, is nothing new (Michi has written about this in the past, see “To Slice or not to Slice” for details). It is nothing more than an exceptionally convoluted, and unreadable, form of an interface definition language. SOAP, WSDL’s partner in crime, is nothing but hype. Finally, the toilet of history has been flushed, and we can watch the last vestiges of the colossal mistake that is web services swirl down the drain.

The lessons of the past are finally being relearned. Slow, fat and bloated is out. Sleek, small and fast is in. Binary encodings and protocols are undergoing a renaissance. All of this is good news for ZeroC and Ice. We understand speed. We live for simplicity.

So where does Google protocol buffers fit in? Other than agreeing with my general world view that binary encodings for non-human readable data are a good thing, Google protocol buffers are only part of a puzzle for many applications. What is missing is a facility to pass a protocol buffer object over the wire. It didn’t take long for some developers to discuss starting such a project. It also didn’t take long for Blair Zajac to point out here and here that Ice would be a good companion.

I shouldn’t have to go on about why using Ice for an RPC mechanism is a good thing. I could trot out all the standard arguments such as speed, flexibility, security, support for both synchronous and asynchronous operations, firewall traversal, quality documentation, and so on and so forth. But I won’t ;)

Using Ice, it is already pretty easy to pass any data that can be encoded to a sequence of bytes, such as the Google protocol buffers.

Lets take a quick look at what this would look like in python.

// .proto
package tutorial;
message Person {
  required int32 id = 1;
  required string name = 2;
  optional string email = 3;
}
// Slice
module Demo
{
  sequence<byte> Person;
  interface Hello
  {
    void sayHello(Person p);
  };
};

In the client:


# python
hello = Demo.HelloPrx….
p = Person_pb2.Person()
# Fill in details
hello.sayHello(p.SerializeToString());

In the server:

# python
class HelloI(Demo.Hello):
  def sayHello(self, s, current=None):
    p = Person_pb2.Person()
    p.ParseFromString(s)
    print "Hello World from %s" % str(p)

Not very complicated, but can we do better? What would be really cool, is if you could pass a Person object as a method parameter, and it would magically appear in the server as a Person object. In other words, automate all that busy work code. Now that would be great!

That is exactly what I’ve spent the past few days doing, and the result is our latest ZeroC Labs project, which you can read about here. As always, source code is readily available.

We’re releasing this labs project as an experiment, to gauge interest. If the community finds it useful, we’ll integrate this more fully into a future release of Ice. It may even be possible to not only support the Google protocol buffers encoding, but also other encodings such as C# and Java serialized types.

Please look over what this integration has to offer, and give us any feedback you might have!

Have fun with Ice, Matthew

Copyright © 2008 ZeroC, Inc.