The purpose of this example is to show many of the capabilities that come with OpenSpaces and to walk the user through a complete application that can easily scale. In addition to the Hello World Example this example introduces the following terms:
Having shown the concept Processing Unit concept in the Hello World Example, we will now show that a PU can contain several services that are independent of each other but serve different purposes within one application. Moreover, different Processing Units use the space to share data, i.e. the same domain model. This is in fact the concept behind SOA and with OpenSpaces and SBA (Space Based Architecture). GigaSpaces provides a SOA platform that can be scalable in a linear manner while providing high performance and low latency.
This example includes two modules that are deployed to the grid, and a domain model that consists of Data objects which are shared between the two modules. Each module runs within a Processing Unit, one runs the DataFeeder bean and the JMSDataFeeder bean that write Data objects with raw data into the remote space. The space is actually embedded within the other Processing Unit, which runs the DataProcessor bean. The DataProcessor service takes the new Data objects, processes the raw data and write them back to the space. Both Processing Units run additional services that interact with the space or with the other services, either in a polling mode, notify mode or remotely.
The entire application looks like this:
The services described above are independent of each other. They are merely explained here to show different capabilities of OpenSpaces.
The only object in our model is the Data object.
Note the annotations that are used in this object:
Basically, every Data object is written to the space by the DataFeeder or by the JMSDataFeeder with the processed value set to false, which is later set to true by the DataProcessor.
Even though our object implements Serializable, it doesn't have to in all cases. This is relevant only when the Data object is used in remote calls.
According to the diagram above, there are 5 different services in our application, each is independent of the others and performs different actions as detailed below. The relevant wiring part, which resides in the relevant pu.xml file (there are two pu.xml files, one for each Processing Unit as shown in the diagram above) is described for each service.
The most interesting part of the code above is the processData() method. Note that it is marked with the @SpaceDataEvent annotation. As you'll see in the wiring part described later, this annotation marks the method to run periodically, take a Data object from the space, perform the business logic of this method (in our case, change the processed attribute to true) and write the object back to the space.
You might also have noticed that the DataProcessor implements IDataProcessor. This simple interface merley reflects the interface of DataProcessor and is used when the service is accessed remotely. In our example, as you can see in the diagram above, the DataRemoting service which is part of one Processing Unit, makes a remote call to the DataProcessor which is located on another Processing Unit (which runs in another process, or even another machine). This is exactly why our Data object is Serializable.
If you take a look at the Configuration tab above, we first define the <bean> of the service, this is a regular Spring configuration. Next we define <os-events:polling-container>, and in it, <os-events:listener>. This is what the annotation in the code is used for. As the listener element merely directs to our service, the @SpaceDataEvent annotation tells the container which method to execute. The polling-container sets the mode of event handling, which by default is a take operation on the space. The <os-core:template> element defines the template to look for in the space (in our case, a Data object with processed=false), as defined in the JavaSpaces specification).
The DataProcessedCounter is a service that shares a Processing Unit with the DataProcessor. It also contains a method that is marked with the @SpaceDataEvent annotation. However, there are two differences between the DataProcessedCounter and the DataProcessor. The minor difference is that the DataProcessedCounter processes Data objects with the processed attribute set to true (meaning objects that were already processed by the DataProcessor and written back to the space).
The major difference is that unlike the DataProcessor, which takes objects from the space to the process, the DataProcessedCounter is also notified about whenever these objects are written to the space or updated in the space. Moreover, instead of receiving notifications on each such events, the DataProcessedCounter receives batches of events that can be based on the number of objects, interval in milliseconds, or both:
The code is only the implementation of the counter. Everything else exists in the configuration, which means you can change the event-handling mode on the fly any way you want.
The <bean> is defined is a similar way, but now we are using an <os-events:notify-container> instead of <os-events:polling-container>, which means our listener is now waiting for notifications. The element <os-events:notify write="true" update="true"/> defines that we're interested only in events of write or update operations on the space. The <os-events:batch size="10" time="5000"/> defines the batch of events per notification, in this case either 10 objects or 5 seconds, which ever comes first.
Unlike the DataProcessor configuration which defines a template of the events we're interested in, using the DataProcessedCounter we define a SQL query, using the class type and and the WHERE clause of the query. The SQL query is equivalent to the way we defined the template, either way can be used.
The DataFeeder is a Spring bean that repetitively (every one second) creates new Data objects with one of the four types (1, 2, 3 or 4), sets the processed value to false, and writes it to the space:
The gigaSpace member is an instance of GigaSpace – the OpenSpaces object that is used to perform operations on the space. Note that the instance is created in a declarative manner:
In the Configuration tab above, we show how to define the space with its URL, and then define the GigaSpace instance which is based on that space. In this case, the space URL points to a remote space location (this is because the space is located within the DataProcessor Processing Unit, which is remote to the Processing Unit in which DataFeeder is running). This approach not only allows you to easily configure the application, but also to completely change the GigaSpace functionality implementation, without changing the code of your original application.
Because the DataFeeder implements Spring's InitializingBean and DisposableBean interfaces, its afterPropertiesSet() and destroy() methods are called when it is created or destroyed, respectively.
The JMSDataFeeder is similar to the DataFeeder. The difference between the beans is that the JMSDataFeeder uses Spring's JmsTemplate on top of the GigaSpaces JMS implementation to write the Data objects to the space; no space API is used directly. This is possible due to the usage of a MessageConverter that converts JMS messages into any required POJO type, in this case, Data. In this example, we configure the ConnectionFactory to use the ObjectMessage2ObjectConverter that comes with the GigaSpaces JMS implementation. The ObjectMessage2ObjectConverter receives a JMS ObjectMessage and returns the message's content (body) as the object to write to the space. The JMS ObjectMessage itself, including headers, properties etc., is not written. The JMSDataFeeder uses Spring's JmsTemplate and MessageCreator to send ObjectMessages that contain the Data objects, and the converter makes sure that only the contained Data objects are written.
The JMSDataFeeder is new in GigaSpaces version 6.0.1 and onwards. For more details on this feature, refer to the JMS-Space Interoperability section.
The JMSDataFeeder is injected with a Spring JmsTemplate. The JmsTemplate is injected with a JMS ConnectionFactory and a destination of type Queue. Unlike the DataFeeder, the JMSDataFeeder does not declare an instance of GigaSpace. GigaSpace is injected into the ConnectionFactory bean and is used behind the scenes by the JMS layer. In addition, the ConnectionFactory is injected with a MessageConverter of type ObjectMessage2ObjectConverter.
Because the JMSDataFeeder implements Spring's InitializingBean and DisposableBean interfaces, its afterPropertiesSet() and destroy() methods are called when it is created or destroyed, respectively.
The ViewDataCounter is a simple service that performs a count operation on the space every second.
Note that the ViewDataCounter is configured in the same pu.xml file as the DataFeeder, however, there is one major difference in how the two services connect to the space. The DataFeeder used a GigaSpace instance to access the space remotely, while the ViewDataCounter uses a different instance, a local view of the space. A local view is like a local filter defined in the client side that registers for specific templates. Every object that is written to the space or updated in it that matches the template is immediately sent to the local view of that client. In this way, different clients can keep different local caches according to their interests.
In this example, the ViewDataCounter is interested only in processed Data objects, so it defines a local view with a template and uses a GigaSpace instance that uses the local view.
Because the ViewDataCounter implements Spring's InitializingBean and DisposableBean interfaces, its afterPropertiesSet() and destroy() methods are called when it is created or destroyed, respectively.
DataRemoting demonstrates the remoting capabilities of OpenSpaces, using the space as the transport layer for the remote calls.
Implementing a remote service is quite straightforward, you just need to expose your remote interface and add the proper configuration. In our example, we want to be able to access the DataProcessor remotely, so we added the IDataProcessor interface. DataRemoting only uses IDataProcessor, and is completely ignorant to the underlying space.
This example includes a build.xml ant file and with a build.bat/sh to execute ant (there is no need to pre-install ant, the ant jars are already bundled in the <GigaSpaces Root>\lib directory).
From the <Example Root> directory (<GigaSpaces Root>\examples\openspaces\data) call:
This compiles the code to a pu directory and copies the Processing Unit Deployment Descriptor, pu.xml.
The Deployment Descriptor should always reside under the META-INF\spring directory of your application.
There are several ways to deploy Processing Units, two of which have been already explained in the Hello World Example. In this example, we will show how to the deploy the Processing Units onto the Service Grid using GigaSpaces Management Center.
After you have run the build, the next phase in order to prepare your deployment is to copy the directories of the Processing Units into the <GigaSpaces Root>\deploy directory.
Under <GigaSpaces Root>\examples\openspaces\data, you can find three directories; common, feeder and processor. The last two contain the two Processing Units; while common contains the common classes that both Processing Units use, in this case the Data object which is our POJO domain model, and the IDataProcessor which is needed for the remoting part.
In each of the other directories (feeder and processor) is a directory called pu\PU_NAME, where PU_NAME is the actual Processing Unit name, in our case, data-feeder and data-processor). These names are later used when deploying the Processing Units to the Service Grid. These are also the two directories you need to copy to <GigaSpaces Root>\deploy. But before you copy them, let's review their contents:
META-INF\spring is where pu.xml resides. shared-lib is the place to put additional classes (in a JAR file) that are used by the Processing Unit (like our Data and IDataProcessor classes, as well as any other external libraries). The rest are the compiled classes of the Processing Unit, under their appropriate package path (which in this example starts with org).
After you copied the two directories, start up the Service Grid. Because this exmaple runs in a partitioned cluster with two primary spaces and one backup space for each partition, you need to run one Grid Service Manager (GSM) and two Grid Service Containters (GSC), and then start the GigaSpaces Management Center:
In the UI, click on the tab named Deployments, Details and the click on the Deploy new application button ().
In the Deployment Wizard, choose SBA Application - Processing Unit and click Next.
Now, all you need to do is type the name of the Processing Unit (identical to the name of the folder that is now in the deploy directory) in the Processing Unit Name field and click Deploy.
Since we have two Processing Units, you need to repeat this process twice, once for the data-processor and then for the data-feeder. There is no need to change any other value in the wizard.
Since the spaces are running within the data-processor, it makes sense to deploy it first and the data-feeder second.
Now, check out the GSC consoles to see the events flowing in and out!
Really liked the example? Wasn't it as helpful as you hoped? Either way, write to us and tell us about it.