Fork me on GitHub

Associations are a way to declare that a service uses (has an association to) another service. Associations define a usage model for services in and across architectures/deployments. Associations can be used either at deployment time to assist in how a service gets created in the context of the service(s) it is associated to, or to wire-up distributed services, injected service references into the declaring service.

For example, a service can declare that it is to be colocated with an associated service, and the service will be created in the same Cybernode as it's associated service. A service can also declare that it is opposed to another service, or isolated from an associated service. The former will ensure the service is not in the same Cybernode, the latter will ensure the associated service is on a different physical machine.

An associated service may also be injected. When association injection occurs the injected service is actually a generated dynamic proxy. The dynamic proxy is used as a way to manage a collection of discovered services. This changed the earlier association injection approach where the declared setter method was invoked each time a service was discovered. The previous approach would also inject a null if all associated services were removed. With the dynamic proxy approach you will only be injected once. The underlying dynamic proxy is updated as services are discovered, removed.

The following sections are included below:

Associations take the following forms:

Association Type Explanation
Uses A weak association relationship where if A uses B exists then, then B may be present for A.
Requires A stronger association relationship where if A requires B exists then B must be present for A. With a type of requires, service lifecycle is considered. Since B must be present for A, A will not be made publicly available (advertised) until B is available. If the associated service connection is broken (as determined by the associated service's Fault Detection Handler) and the Association type is Requires, the service will be unadvertised.
Colocated An association that requires that A be colocated with B, that they be created in the same JVM (Cybernode). If B does not exist, or cannot be located, A shall not be created without B. If either A or B is forked or exec'd, they will be colocated on the same physical machine, created by the same Cybernode.
Opposed An association which requires that A exist in a different JVM then B.
Isolated An association which requires that A exist in a different physical machine then B.

Scope

Association declaration scope is either globally or service specific. Declaring an association whose enclosing parent is a service (using groovy) or ServiceBean (using xml) scopes it as service specific. If declared outside of these elements the association is global, meaning all services in the opstring will have that association.

Examples

  • Declaring an association to a service within the opstring:
        association(type: 'uses', name: 'Foo', property: 'foo')
    

    The declaration above declares that the service with the name of 'Foo' be injected using the 'setFoo' setter into your service.



  • Declaring an association that matches a service based on the service's exported proxy type and name:
        association(type: 'uses', name: 'Foo', property: 'foo', serviceType: 'example.Foo')
    

    The declaration above is similar to the previous declaration, except it will discover service(s) that are available on the network that are not declared in your opstring, and match the service's exported proxy type and name.



  • Declaring an association that matches a service based on the service's exported proxy type, and does not match on the service's name:
        association(type: 'uses', name: 'Foo', property: 'foo',
            serviceType: 'example.Foo', matchOnName: false)
    

    The declaration above is similar to the previous declaration, e xcept it will discover service(s) that are available on the network that are not declared in your opstring, and match the service's exported proxy.

If associations have been declared with the 'property' attribute, discovered services will be injected into your service using an IoC approach. Based on the setter method's signature, the injection type differs.

If the setter method simply declares that the service type be injected:

public void setFoo(Foo foo) {
    this.foo = foo;
}

The proxy that is injected will be a dynamic proxy. Details on the dynamic proxy options can be found below in the Service Selection Strategies section.

If you are using a dynamic proxy and would like to get the 'raw' proxy, the AssociationProxyUtil.getService() method can be used.

A setter method can also be declared to give you control over the collection of services:

public void setFoos(Iterable<Foo> foos) {
    this.foos = foos;
}

Then when you need to access a discovered service in your code, you can simply iterate over the collection:

for(Foo foo : foos) {
    // do something with a Foo
}

You can also have the Association itself injected:

public void setFooAssociation(Association<Foo> foo) {
    this.foo = foo;
}

Armed with the Association class, you can interact with the collection of discovered services as you wish.

The Association class also provides the support to obtain a Future, through the Association.getServiceFuture() method. You may choose to use this approach when declaring an association to be injected with the an attribute of inject: 'lazy'

All association proxies have a service selection strategy. The service selection strategy provides a way to determine how services in the collection of discovered services are invoked. The current service selection strategies are fail-over, round-robin and utilization (note all service selection strategies will also prefer local services over remote services).

  • The Fail-Over strategy will invoke the first service in it's collection for each method invocation until that service is no longer reachable. If the associated service is unavailable, the fail-over strategy invokes the next service in it's list.
  • The Round Robin strategy alternates selection of services from the association, alternating the selection of associated services using a round-robin approach.
  • The Utilization strategy is a round-robin selector that selects services running on compute resources whose system resources are not depleted. System resource depletion is determined by org.rioproject.system.MeasuredResource provided as part of the org.rioproject.system.ComputeResourceUtilization object returned as part of the deployment map. If any of a Cybernode's resources are depleted, the service hosted in that Cybernode will not be invoked. This is of particular use in cases where out of memory conditions may occur. Using the utilization strategy a service running in a memory constrained Cybernode will not be invoked until the JVM performs garbage collection and memory is reclaimed.

An example of the declaration is as follows:

association(type: 'uses', name: 'Foo', property: 'foo') {
    management strategy: Utilization.name
}

Associations can be injected in a lazy or eager mode. Lazy injection is the default, and injection occurs when a service is discovered. Eager injection occurs immediately, even if there are no discovered services.

Associations must also deal with proxy failure modes. In a typical distributed environment, if there are no discovered (available) services and a remote method invocation is attempted on a service, a RemoteException is thrown. This seems to make sense, although a better approach may be in order. Perhaps the dynamic proxy can either wait until a service is discovered to make the invocation, or provide at least a retry or timeout. It would seem that the invoking client would need to do this in any case, and from a coding point of view this behavior would be in the generated proxy, not in the application code. The difference here is in a fail-fast approach, service invocation will fail in an immediate and visible way, allowing the caller to be notified immediately that something is wrong. In our context here, we would not immediately raise the RemoteException (at least not right away), we would go onto the next service. We would fail-fast if there are no services available. In association terminology, the association would be broken.

With a fail-safe approach, we would want to constrain the notification that no services are available (as opposed to failing fast by throwing a RemoteException) in a managed way having the association proxy retry for a certain period of time. In this case the fail-safe mode would have the caller blocking on the service's remote method invocation until a service becomes available, or until the retry logic exhausts itself. If the retries are exhausted and there are still no available services, a RemoteException will be thrown.

The following snippet shows the declaration of an association that allows for a fail-safe approach by controlling the time the generated association proxy will wait for a service to become available:

association(type: 'uses', name: 'Foo', property: 'foo') {
    management strategy: Utilization.name
    serviceDiscoveryTimeout: 5
}

As a default there is no service discovery timeout. This means that your association proxy (by default) is injected lazily when the associated service(s) are discovered. If there are associated services are removed, and there are no associated services, then the association proxy will "fail-fast".

If you configure that the association proxy is injected eagerly, that is immediately upon service creation, you really need to consider declaring a service discovery timeout.

Regardless of the injection style (lazy or eager), you can control how the association proxy handles the association broken scenario (no associated services discovered) by configuring the service discovery element.

The Association framework can also assist in the development of clients that need to discover services. Using Associations from a client provides the same benefits of service-oriented associations:

  • Use of concurrent utilities, using a Future to address asynchronous service discovery
  • Proxy architecture providing support for Association strategies
  • Less code to master and write

From a client perspective, you need to do the following:

  1. Setup your AssociationDescriptors
  2. Create the AssociationManagement object
  3. Add the AssociationDescriptor, and get the Association object
  4. Using the Association, get the Future for the service

For discussion purposes, lets reference the test case included in the Calculator example. The test class name is ITCalculatorClientTest. Lets refer to the setup() method:

/*
 * Call the static create method providing the service name, service
 * interface class and discovery group name(s). Note the the returned
 * AssociationDescriptor can be modified for additional options as well.
 */
 AssociationDescriptor descriptor = AssociationDescriptor.create("Calculator",
                                                                  Calculator.class,
                                                                  testManager.getGroups());
  /* Create association management and get the Future. */
  AssociationManagement aMgr = new AssociationMgmt();
  Association<Calculator> association = aMgr.addAssociationDescriptor(descriptor);
  future = association.getServiceFuture();

The test method simply uses the returned future to get the Calculator service:

Calculator service = future.get();
Assert.assertNotNull(service);
testService(service);

Running the test successfully requires that the Calculator service is obtained.

Back to top

Version: 5.6. Last Published: 2017-01-01.