Callbacks Through Glacier2
8 min read
8 min read
8 min read
8 min read
8 min read
8 min read
8 min read
8 min read
8 min read
Callbacks from servers to clients are commonly used in distributed applications, often to notify the client about an event such as the completion of a long-running calculation or a change to a database record. Unfortunately, supporting callbacks in a complicated network environment presents its own set of problems. Ice overcomes these obstacles using a Glacier2 router and bidirectional connections.
Bidirectional Connections with Glacier2
While a regular unrouted connection allows requests to flow in only one direction (from client to server), a bidirectional connection enables requests to flow in both directions. This capability is necessary to circumvent the network restrictions that commonly cause firewall traversal issues, namely, client-side firewalls that prevent a server from establishing an independent connection directly to the client. By sending callback requests over the existing connection from the client to the server (more accurately, from the client to the router), we have created a virtual connection back to the client.
This diagram shows the steps involved in making a callback using Glacier2:
Request and callback flow through Glacier2.
- The client has a routed proxy for the server and makes an invocation. A connection is established to the router's client endpoint and the request is sent to the router.
- The router, using information from the client's proxy, establishes a connection to the server and forwards the request. In this example, one of the arguments in the request is a proxy for a callback object in the client.
- The server makes a callback to the client. For this to succeed, the proxy for the callback object must contain endpoints that are accessible to the server. The only path back to the client is through the router, therefore the proxy contains the router's server endpoints. The server connects to the router and sends the request.
- The router forwards the callback request to the client using the bidirectional connection established in step 1.
The arrows in the above illustration indicate the flow of requests; notice that two connections are used between the router and the server. Since the server is unaware of the router, it does not use routed proxies, and therefore does not use bidirectional connections.
Callbacks and Connection Closure
When a client terminates, it closes its connection to the router. If a server later attempts to make a callback to the client, the attempt fails because the router has no connection to the client over which to forward the request. This situation is no worse than if the server attempted to contact the client directly, which would be prevented by the client's firewall. However, this illustrates the inherent limitation of bidirectional connections: the lifetime of a client's callback proxy is bounded by the lifetime of the client's router session.
Configuring the Router for Callbacks
In order for the router to support callbacks from servers, it needs to have endpoints in the private network.
The configuration file shown below adds the property Glacier2.Server.Endpoints:
Glacier2.Client.Endpoints=tcp -h 5.6.7.8 -p 4063Glacier2.Server.Endpoints=tcp -h 10.0.0.1As this example shows, the server endpoint does not require a fixed port.
Configuring the Client's Object Adapter with a Router
A client that receives callbacks is also a server, and therefore must have an object adapter. Typically, an object adapter has endpoints in the local network, but those endpoints are of no use to a server in our restricted network environment. We really want the client's callback proxy to contain the router's server endpoints, and we accomplish that by configuring the client's object adapter with a proxy for the router.
We supply the router's proxy by creating the object adapter with createObjectAdapterWithRouter, or by defining the object adapter property adapter.Router as shown below:
CallbackAdapter.Router=Glacier2/router:tcp -h 5.6.7.8 -p 4063The direct proxies an object adapter creates contain its published endpoints. For an object adapter configured with a router, the published endpoints are the published endpoints of the router's server object adapter, Glacier2.Server.
An object adapter configured in this way allows the client to receive callback requests via the router. If the client also wants to service requests via local (non-routed) endpoints, the client must create a separate adapter for these requests.
Callback Object Identities
Glacier2 assigns a unique category to each client for use in the identities of the client's callback objects. The client creates proxies that contain this identity category and pass these proxies to back-end servers for use in making callback requests to the client. This category serves two purposes:
- Upon receipt of a callback request from a back-end server, the router uses the request's category to identify the intended client.
- The category is sufficiently random that, without knowing the category in advance, it is practically impossible for a misbehaving or malicious back-end server to send callback requests to an arbitrary client.
A client can obtain its assigned category by calling getCategoryForClient on the Router interface as shown in the example below:
Glacier2::RouterPrx router{...};optional<Glacier2::SessionPrx> session = router->createSession(...);// Retrieve the client category after the session is created.string category = router.getCategoryForClient();Glacier2.RouterPrx router = ...;Glacier2.SessionPrx? session = await router.createSessionAsync(...);// Retrieve the client category after the session is created.string clientCategory = await router.getCategoryForClientAsync();RouterPrx router = ...;session = router.createSession(...);// Retrieve the client category after the session is created.String clientCategory = router.getCategoryForClient();const router = new Glacier2.RouterPrx(...);const session = await router.createSession(...);// Retrieve the client category after the session is created.const clientCategory = await router.getCategoryForClient();router = Glacier2.RouterPrx(...)session = await router.createSessionAsync(...)# Retrieve the client category after the session is created.clientCategory = await router.getCategoryForClientAsync()let router = try makeProxy( communicator: communicator, proxyString: "...", type: Glacier2.RouterPrx.self)let session = try await router.createSession(...)// Retrieve the client category after the session is created.let clientCategory = try await router.getCategoryForClient()Nested Invocations with a Router
If a router client intends to receive callbacks and make nested twoway invocations, it is important that the client be configured correctly. Specifically, you must increase the size of the client thread pool to at least two threads.
Handling Connection Loss
If the client loses its connection to the Glacier2 router, its session ends, and the router no longer forwards callback requests for that session's category. To receive callbacks again, the client destroys its callback object adapter and starts over with a new session: the new session has a new category, so the client creates its callback objects with new identities and passes the new callback proxies to the back-end servers.