Skip to content

Service Bus (Details)

Carsten Stocklöw edited this page Apr 25, 2018 · 10 revisions

Table of Contents

Introduction

In a previous previous chapter, we have investigated the first of the three communication channels of the universAAL platform, the Context Bus. The Context Bus is responsible for transferring "contextual information" between running applications, which basically means that the Context Bus is used for sharing knowledge within an universAAL-based system. This chapter is dedicated to another one of the Middleware buses, the Service Bus. The Service Bus passes so-called "Service Requests" from one application to another, in which the requesting application specifies a functionality that it would like another application to perform, unbeknownst of whether such a service is actually available at the time of its request. In this regard, the Service Bus is used for sharing functionality among a distributed system's nodes. Because the requester of a service usually needs to know, whether the requested service has been performed or not, the Service Bus is a call-based communication channel, which makes it a bit more complicated to work with than the event-based, "fire and forget"-natured Context Bus.

So, that's what you call service?

Before we begin to talk about applications that request or offer services (Service Callers and Service Callees, respectively), we should probably try to find a mutual understanding of the term "service" first. Alas, the term is a bit hard to grasp, though. In the following, when we use the term "service" (note the small s), we will mean a process in which somebody is performing something for somebody else. When we refer to a "Service" (with a leading capital S), however, we will refer to specific ontology class (from the package org.universAAL.middleware.service.owl), which is going to be the root class of all the custom services that we will define for our applications. In other words: whenever we write a service class in universAAL, it will inherit from this superclass (directly or indirectly).

This already implies that we will rely on ontologies not just for the sharing of knowledge, but also for the specification of functionalities. More specifically: we will use an ontological model to be able to unambiguously define, what an application can do for other applications - and what it can't do. The approach has various benefits and there's a specific one that we want to highlight at this point (but we will get back to the others in a bit). Think of a TV and think of the services that such a device could have to offer. First of all, a TV can be turned on and off. It also has a volume control (which means that its volume can be turned up, down, and off) and a TV is (most likely) capable of showing various TV channels (which means you can switch channels). So far, so obvious. Imagine that our TV is located in the bedroom of an apartment and that an application sends a request over the Service Bus, asking that positively all light sources in the bedroom must be switched off (possibly because the user stated that she suffers from a migraine attack). Because our ontological models specifies, that the TV has a backlit screen (the ontology class "TV" has a property that refers to another ontology class, "BacklitScreen"), and because this backlit screen qualifies as a light source (it inherits from the ontology class "LightSource"), the Service Bus determines that besides the lamps, the TV also generates brightness - and that it needs to be turned off, too, if the user wants positively no light sources at all. This built-in reasoning ability of the Service Bus is one of the benefits that come with using an ontological model to describe a device's capabilties, rather than relying on classical syntactic interfaces.

How the Service Bus works

Just as with the Context Bus, there are two types of applications that communicate with the Service Bus. The so-called "Service Callees" are those applications that are willing to provide services to other applications. On the other hand, there are the "Service Callers" applications that would like one or more applications to perform a service on their behalf. To this end, they send an appropriate "Service Request" to the Service Bus in which they define the functionality they require. The Service Bus then checks the available "Service Profiles" that the Service Callees have registered in order to find all those among them, that are capable of performing the requested service. If the Service Bus finds one or more candidates, it forwards copies of the Service Request to them. Normally, the Service Bus then waits for all of the Service Callees to perform the requested service and to send an appropriate reply back to the bus. Once all replies have been returned, the Service Bus sends the lot of them back to the Service Caller.

In a way, the Service Bus works similar to a broker on a stock exchange. There are those who want to buy, the Service Callers, and there are those that want to sell, the Service Callees. Because the market situation is too complex for an individual buyer or seller to find all possible trading partners and to efficiently determine the best option, they turn to a broker, who gathers all buying and selling proposals. The broker then decides for the best way how to match buying and selling proposals such that the highest possible amount of them can be settled. This is exactly, what the Service Bus does: it arranges "deals" between buyers, the Service Callers, and sellers, the Service Callees.

The following list shows the individual steps of the communication process (see also the following Figure). The two Service Callee applications E1 and E2 offer services and the Service Caller application R1 requests the performance of a service by sending an appropriate Service Request to the Service Bus.

  • The two Service Callees E1 and E2 register Service Profiles at the Service Bus. They use these to specify to the bus, which services they intend to offer (1).
  • The Service Caller application R1 sends a Service Request to the Service Bus in which it states, what kind of service it requires another application to perform (2).
  • The Service Bus checks the Service Request from Service Caller R1 against all Service Profiles that have been registered by Service Callees. It finds that both Service Callee E1 and E2 are capable of performing the requested service (3) and thus forwards copies of the Service Request send by Service Caller R1 to both of them. At this stage it is no longer a "request" as some real services have been found and will be explicitely called. Hence, the object they receive is not an instance of the class ServiceRequest, but of the class ServiceCall (4).
  • Service Callee E1 quickly performs the requested service and sends an appropriate Service Response back to the Service Bus (5).
  • Service Callee E2 fails to perform the requested service within the available timeframe (possibly because the application terminated) (marked by a red cross).
  • The Service Bus aggregates the service responses from all services it has called, in this case only from Service Callee E1 (6).
  • The Service Bus sends the aggregated Service Response back to Service Caller R1 and also notifies it that another Service Callee failed to respond to the request in time (7).

Service Callees and Service Profiles

This tutorial is available at samples/tutorials/tut.service.bus.callee

Service Callees are applications that offer services to other applications. They do this by registering appropriate Service Profiles at the Service Bus. These Service Profiles define, what services a specific Service Callee is capable of performing. All Service Callees must inherit (directly or indirectly) from the parent class ServiceCallee. Let's start with a simple Service Callee stub. By changing the name, you can reuse this stub for all of your custom Service Callee applications.

public class MyServiceCallee extends ServiceCallee {

  protected MyServiceCallee(ModuleContext context) {
    super(context, getProfiles());
  }

  public static ServiceProfile[] getProfiles() {..}

  public void communicationChannelBroken() {..}

  public ServiceResponse handleCall(ServiceCall call) {..}
}

Pretty straightforward code, thus far. Our class inherits from ServiceCallee, has a constructors and two mandatory methods (the method getProfiles() can also be realized as method of a different class). You have already encountered one of them, that's the communicationChannelBroken()-method. It has the same functionality as it has in for the Context Subscriber classes (check the chapter about Context-Bus-(Details)), meaning that it gets called automatically when our class loses connection to the bus. We will leave it blank again, but you definitely want to put some error handling here for your own applications (which, for example, could try to reestablish the connection or just log the error). The second mandatory method is handleCall(). This method gets called by the Service Bus when a Service Request matches the Service Profile that our Service Callee registered at the bus. In other words: this is where we need to trigger the execution of the service that we offered. We will get back to this method in a bit.

Note that the constructor does nothing but to simply call the constructor of the parent class, handing it both a ModuleContext object (just passing it on) and an array of the type ServiceProfiles that it retrieves from the static method getProfiles(). You can also define the profiles in another class, usually called ProvidedXXService. Using such a helper class for the definition of our Service Profiles allows us to classes small. Note that this is not a must, though. If you prefer, you can define the Service Profiles for your Service Callee application in the same class and this is perfectly fine.

Before we define the service profiles, we need to define some URI's. In this example, we only provide a single service to turn on a light actuator. This service needs as input the URI of that actuator, so we also define a URI for that input parameter.

 public static String SERVICE_TURN_ON = "urn:org.universAAL.tutorial:tut.callee#srvTurnOn";
 public static String INPUT_LIGHT_URI = "urn:org.universAAL.tutorial:tut.callee#inLampURI";

Now let's take a look at the method getProfiles() that will define the profile of the service we want to offer to other applications.

public static ServiceProfile[] getProfiles() {
   Service turnOn = new DeviceService(SERVICE_TURN_ON);
   turnOn.addFilteringInput(INPUT_LIGHT_URI, LightActuator.MY_URI, 1, 1,
      new String[] { DeviceService.PROP_CONTROLS });
   turnOn.getProfile().addChangeEffect(
      new String[] { DeviceService.PROP_CONTROLS, ValueDevice.PROP_HAS_VALUE },
      new Integer(100));
   return new ServiceProfile[] { turnOn.getProfile() };
}

Sticking to our LightActuator example, we want to implement a Service Callee that controls a light actuator, maybe a lamp in the living room. We will go with a simple light here, one that can only be turned on. Naturally, you would also have a service to turn the light off, and a service to query the set of devices that this service manages. As can be seen in the last line, the method getProfiles() returns an array of all the profiles of our service callee; thus, you can define an arbitrary number of profiles.

We start our service by creating a new instance of DeviceService by passing the URI we want to use for the service (SERVICE_TURN_ON). All further definitions are based on this Service class of the appropriate ontology, i.e. we will define at which property path (in the RDF-Graph, starting from the Service class) we expect a certain input, effect, or output. In this example, the turnOn service defines that

  • at property path controls needs to be an input of type LightActuator. It must be exactly one value (cardinality is min=1 and max=1) and the input can be retrieved with the URI INPUT_LIGHT_URI.
  • the value at property path controls - hasValue needs to be changed to 100 (in percent).
Maybe you ask yourself at this point "Why do I need to have this profiles defined in this way?". Here we have one main feature using the semantic middleware universAAL. Later, when services are requested, we do not need to give the concrete Name of a profile (and therefore a special function to perform). To a request we will only add the inputs we want to give and the outputs we expect and the middleware use the profile (from e.g. all available services based on DeviceService) that best fits to our needs. This allows us to decouple caller and callee completely. Last thing to do is to save the profile in an array and return this array.

Now we come to the "core" of every Service Callee - its handleCall()-method:

public ServiceResponse handleCall(ServiceCall call) {
   String operation = call.getProcessURI();
   if (operation.startsWith(SERVICE_TURN_ON)) {
      Object input = call.getInputValue(INPUT_LIGHT_URI);
      System.out.println("Received service call with parameter " + input);
      return new ServiceResponse(CallStatus.succeeded);
   }
   return new ServiceResponse(CallStatus.serviceSpecificFailure);
}

To handleCall every valid Service Request at the Service Bus that matches one of our Service Profiles is forwarded. Here we use the defined URIs of the services to determine the needed request. With call.getProcessURI() we get the URI of the profile that had best fit to the original request passed from the Service Bus (if there were any similarities) to the local node. First the class ServiceCall provides the function getProcessURI, which is at least in the prefix similar to the Service-URIs. Therefore we simply have to show if it corresponds to our defined profiles and create/return an appropriate response. For SERVICE_TURN_ON, we have an input and we can get this input by calling getInputValue and passing the URI of the input we want to have.

In our simple example, the service does not have a return value. Thus, the response is just the status of executing the service, i.e. it is either CallStatus.succeeded or CallStatus.serviceSpecificFailure.

Let's assume we would define another service profile, SERVICE_GET_CONTROLLED_LIGHTS, that will return a list of all the light actuators that our application manages. The service profile would look like this:

 Service getAll = new DeviceService(SERVICE_GET_CONTROLLED_LIGHTS);
 getAll.addOutput(OUTPUT_LIGHTS, LightActuator.MY_URI, 0, -1,
    new String[] { DeviceService.PROP_CONTROLS });

And the service response would look like this:

 ServiceResponse sr = new ServiceResponse(CallStatus.succeeded);
 sr.addOutput(new ProcessOutput(OUTPUT_LIGHTS, controlledLights));

where 'controlledLights' would be a java.util.List of all the light actuators.

Again, when you don't need the callee anymore, you need to close it to free all resources. However, if you need to receive multiple service requests, you should re-use your callee object.

 callee.close();

Service Callers and Service Requests

This tutorial is available at samples/tutorials/tut.service.bus.caller

Now that we have our Service Callee up and running, it's time to implement an application that actually requests the services that our Callee has to offer, a Service Caller. The Service Caller does this by sending a Service Request to the Service Bus in which it specifies, what the services are that it would like Service Callees to perform on its behalf. For our exemplary application, we want a Service Caller that requests a specific light actuator to change its (brightness) value. You can connect this Service Caller application to a simple Java Swing GUI and thus trigger the corresponding change requests by simple button clicks. The following code provides an example for such a Service Caller class.

 ServiceCaller caller = new DefaultServiceCaller(mc);
 ServiceRequest turnOn = new ServiceRequest(new DeviceService(), null);
 turnOn.addValueFilter(new String[] { DeviceService.PROP_CONTROLS },
    new LightActuator("urn:org.universAAL.space:KitchenLight"));
 turnOn.addChangeEffect(
    new String[] { DeviceService.PROP_CONTROLS, ValueDevice.PROP_HAS_VALUE },
    new Integer(100));
 caller.call(turnOn);

Note that we are instantiating an object of the type DefaultServiceCaller, rather than an object of the type ServiceCaller. DefaultServiceCaller is a convenience class that somewhat simplifies the handling of Service Callers and that should be sufficient for most requirements.

We create a Service Request that requests to turn on the light actuator, i.e. setting its (brightness) value to 100 (in percent). First, we create the Service Request by passing the class of services we are interested in. In this case, it's the DeviceService. The second parameter specifies the exact user that is involved in the Service Request. We will leave this parameter unspecified for now (just put "null"). The remaining two settings of the turnOn request are very much similar to the seervice profile we have specified above for the Service Callee:

  • at property path controls needs to be (exactly one) resource with the URI "urn:org.universAAL.space:KitchenLight" that is of type LightActuator
  • the value at property path controls - hasValue needs to be changed to 100 (in percent).
The last line will actually call the service(s) by providing the Service Request to the service bus via the DefaultServiceCaller.

In this simple example, we are not interested in any return value. We could, however, at least check whether the request was successful by changing the code to this:

 ServiceResponse sr = caller.call(turnOn);
 if (sr.getCallStatus() == CallStatus.succeeded)
    ...

A potential service request would be to retrieve a list of all light actuators.

 ServiceRequest getAll = new ServiceRequest(new DeviceService(), null);
 getAll.addTypeFilter(new String[] { DeviceService.PROP_CONTROLS },
    LightActuator.MY_URI);
 getAll.addRequiredOutput(OUTPUT_LIST_OF_LIGHTS,
    new String[] { DeviceService.PROP_CONTROLS });

First, we add a type filter at the property path 'controls'. This means that we only want to call services that control LightActuators. This is required because the DeviceService and its property PROP_CONTROLS are very generic and are used for all kinds of devices. If we would omit this type filter, we would get a list of all devices. Then, we add a required output at the same path together with a URI (OUTPUT_LIST_OF_LIGHTS) that will later be used to retrieve the list of lights from the service response.

Equivalent to the context bus publisher:
when you don't need to send any more service requests, you need to close the caller to free all resources. However, if you need to send multiple service requests, you should re-use your caller object.

 caller.close();

Tryout

This tutorial is available at samples/tutorials/tut.service.bus.tryout

Equivalent to the Context Bus Tryout, the Tryout project is a combination of caller and callee, and is meant to experiment with the parameters of the service bus matchmaking.

Support:

Found a problem?
  • Report suggestions, missing, outdated or wrong documentation creating an Issue with "documentation" tag
Clone this wiki locally