Internet Communication Engine (Ice) Basics

Ice Principles

  • Distributed communication between several computers and processes using TCP or UDP coordinated by a service called IceGrid
  • Interface Definition Language (IDL) to describe interfaces in a uniform way
  • Remote Procedure Calls using proxies (sync and async)
  • Publish/Subscribe mechanism realized through the IceStorm server which uses topics for communication

Official Webpage: http://www.zeroc.com/ Ice Documentation: http://doc.zeroc.com/display/Ice/Ice+Manual

Object Types in Ice

Anonymous Ice objects:

  • Objects are not explicitly registered with IceGrid
  • Objects only reachable through explicit proxy passing

Ice "well-known objects":

  • Objects are registered with IceGrid using a unique name
  • The unique object names can be used to query IceGrid/Locator for object proxies

Changing an interface without breaking running systems

ArmarX is a distributed system in which the participating processes are built from different workspaces, on different machines, at different times. An .ice file is therefore not an internal detail of one package: it is a contract with every peer that is currently deployed, including the ones nobody plans to rebuild today.

Never add a member to a struct

Adding a member to a struct is an unconditional, silent wire break for every peer that has not been rebuilt. Ice encodes struct members back to back, with no type id, no member count and no tags, and members of a struct cannot be declared optional. There is nothing in the message that would let a reader notice that the sender used an older definition – it simply keeps reading and walks off the end of the message, or, worse, off into the following parameter.

This applies to every struct that crosses the wire, whether it is a parameter, a return value, or nested inside a sequence or dictionary. It applies in both directions: removing a member and reordering members break just as badly.

The observable result is Ice::UnmarshalOutOfBoundsException, raised in the newer process, blaming an operation that is not itself at fault.

What to do instead

To add information to an existing call, in rough order of preference:

  • Add an optional(n) operation parameter. Unlike struct members, operation parameters can be tagged; a caller that does not send the parameter is understood by a newer server, which sees it as "not set" and can fall back to the previous behaviour. Tag numbers must never be reused.
  • Put the information inside a self-describing payload that is already being passed, such as an ARON aron::data::dto::Dict. Older peers pass the dict through untouched and simply do not contain the new key, which readers must treat as absent rather than as an error.
  • Add a new operation alongside the old one and keep the old one working, if the change is large enough that neither of the above stays readable.

Whichever route you take, the new information must be optional in meaning, not only in encoding: there has to be a sensible interpretation of "an older peer did not send this". If no such interpretation exists, the change is a new interface version, and every participant has to be rebuilt and restarted together.

A note on classes

Ice class types are versioned on the wire (they carry a type id and support slicing and optional members), so they can be extended more safely than structs. That is not a reason to convert existing structs to classes casually – classes are heavier and change the generated C++ API – but it is the reason the rule above is about structs specifically.