Custom Change
A custom change operation lets the user implement his own change operation in case the built-in operations (increment, add, remove, set, etc.) do not suffice. This is a very powerful capability but it must be used with extreme caution.
Implementing and Using a Custom Change Operation
The implementation should extend the abstract CustomChangeOperation
class and implement both the getName
and change
methods.
See below an example of a change operation which multiplies an integer property value:
public class MultiplyIntegerChangeOperation extends CustomChangeOperation {
private static final long serialVersionUID = 1L;
private final String path;
private final int multiplier;
public MultiplyIntegerChangeOperation(String path, int multiplier) {
this.path = path;
this.multiplier = multiplier;
}
@Override
public String getName() {
return "multiplyInt";
}
public String getPath() {
return path;
}
public int getMultiplier() {
return multiplier;
}
@Override
public Object change(MutableServerEntry entry) {
//Assume this is an integer property, if this is not true an exception will be thrown
//and the change operation will fail
int oldValue = (Integer)entry.getPathValue(path);
int newValue = oldValue * multiplier;
entry.setPathValue(path, newValue);
return newValue;
}
}
Using it will be like any other change operation, while providing this custom implementation:
gigaSpace.change(query, new ChangeSet().custom(new MultiplyIntegerChangeOperation("votes", 2)));
With Java 8 lambda syntax the above can be done in a simpler way and without extending the CustomChangeOperation
interface:
gigaSpace.change(query, new ChangeSet().custom("multiplyInt", (entry) -> {
//Assume this is an integer property, if this is not true an exception will be thrown
//and the change operation will fail
int oldValue = (Integer)entry.getPathValue("votes");
int newValue = oldValue * 2;
entry.setPathValue("votes", newValue);
return newValue;
}));
Disabling the Path Caching
By default, the Change API caches paths that are updated repeatedly in order to improve performance. However, this may increase memory consumption more than usual in certain cases, such as when the path is dynamic. To prevent memory leaks, you can disable the path caching.
The following code example shows how to invoks the disablePathCaching()
method on any mutator instance (inherited from the SpaceEntryPathMutator
class).
ChangeSet changeSet = new ChangeSet().custom(
new IncrementSpaceEntryMutator("myMapField.myTestKey2",0.1).disablePathCaching() );
gigaSpace.change(new IdQuery<>(MyTestObject.class, 1L, 1L ), changeSet);
When implemented as demonstrated in the above code, any path that is provided by IncrementSpaceEntryMutator
won't be cached.
The Name of a Custom Change Operation
The custom operation is treated like the built-in change operations (in fact the build in implementations are using the same mechanism), therefore the operation should have a unique name which is used in all the relevant places as described in the Change API Advanced Page, such as configuring which operations are supported by a SpaceSynchronizationEndpoint
implementation, using it inside space and replication filters to identify which custom change operation is executed, etc.
Mandatory Implementation Requirements
When implementing a custom change operation, the following guidelines must be followed:
The provided MutableServerEntry
is wrapping the actual object which is kept in space, therefore it is crucial to understand when a value is retrieved from the entry
it points to the actual reference in the data structure of the Space Where GigaSpaces data is stored. It is the logical cache that holds data objects in memory and might also hold them in layered in tiering. Data is hosted from multiple SoRs, consolidated as a unified data model.. The content of this reference should not be changed as it will directly affect the object in Space and will break data integrity, transaction management and visibility scoping (e.g. transaction abort will not restore the previous value). Changing a value should always be done via the MutableServerEntry.
Moreover, if you want to change a property within that value by invoking a method on that object (e.g. if the value is a list, adding an item to the list), you must first clone the fetched value, and then invoke the method on the cloned copy. Otherwise, you will change the existing data structure in the space without going through the proper data update mechanism and will potentially break data integrity.
Below you can find an example that adds the element 2 into an ArrayList that exists in the entry under a property named "listProperty". The result sent to client (if requested) is the size of the collection after the change. Note that we clone the ArrayList before modifying it as explained above.
public Object change(MutableServerEntry entry) {
ArrayList oldValue = (ArrayList)entry.getPathValue("listPropery");
if (oldValue == null)
throw new IllegalStateException("No ArrayList instance exists under the given path
'listProperty', in order to add a value an ArrayList
instance must exist");
Collection newValue = (ArrayList)oldValue.clone()
newValue.add(2);
int size = newValue.size();
entry.setPathValue("listProperty", newValue);
return size;
}
getPathValue
, setPathValue
operations support nested paths, it will traverse on properties and map keys if the path contains ".' in it (e.g. "myPojo.mapProperty.key")
When using a replicated topology (e.g. with backup space instances, gateways, mirror) the change operation itself is replicated (and NOT the modified entry). Hence, it is imperative that this method will always cause the exact same affect on the entry (assuming the same entry was provided). For example it should not rely on variables that may change between executions, such as system time, random, machine name etc. If the operation is not structured that way, the state can be inconsistent in the different locations after being replicate.
Space Security
Custom change operation lets you run custom code on the space, hence the space security privilege required in order to execute a custom change operation is EXECUTE
(the same role which allows executing space tasks).
Custom Operation and Space Integration Points
Using a custom operation with a Replication Filter, Space Filter and Space Synchronization Endpoint is supported
and behaves the same as the built-in operations. You can get a reference to the instance of the CustomChangeOperation
by checking its name (or instanceof
) and casting to the specific type.
DataSyncChangeSet dataSyncChangeSet = ChangeDataSyncOperation.getChangeSet(dataSyncOperation);
Collection<ChangeOperation> operations = dataSyncChangeSet.getOperations();
for(ChangeOperation operation : operations) {
if (operation.getName().equals("multiply") {
String path = ((MultiplyIntegerChangeOperation)operation).getPath();
int multiplier = ((MultiplyIntegerChangeOperation)operation).getMultiplier();
// ... do something with the path and multiplier
}
//...
}
Change code without restarts
When executing a change over the space, the code is loaded from the remote client and cached for future executions. Since the code is cached, modifications are ignored, and users are forced to restart the space whenever they modify the code.
Starting with 12.1, you can use the @SupportCodeChange
annotation to tell the space your code has changed.
The space can store multiple versions of the same task. This is ideal for supporting clients using different versions of a task.
For example, start with annotating your task with @SupportCodeChange(id="1"), and when the code changes, set the annotation to @SupportCodeChange(id="2"), and the space will load the new task.
import com.gigaspaces.annotation.SupportCodeChange;
import com.gigaspaces.client.CustomChangeOperation;
import com.gigaspaces.server.MutableServerEntry;
@SupportCodeChange(id="1")
public class MultiplyIntegerChangeOperation extends CustomChangeOperation {
private static final long serialVersionUID = 1L;
private final String path;
public MultiplyIntegerChangeOperation(String path) {
this.path = path;
}
@Override
public String getName() {
return "multiplyInt";
}
public String getPath() {
return path;
}
@Override
public Object change(MutableServerEntry entry) {
int oldValue = (Integer) entry.getPathValue(path);
int newValue = oldValue * 10;
entry.setPathValue(path, newValue);
return newValue;
}
}
import com.gigaspaces.annotation.SupportCodeChange;
import com.gigaspaces.client.CustomChangeOperation;
import com.gigaspaces.server.MutableServerEntry;
@SupportCodeChange(id="2")
public class MultiplyIntegerChangeOperation extends CustomChangeOperation {
private static final long serialVersionUID = 1L;
private final String path;
public MultiplyIntegerChangeOperation(String path) {
this.path = path;
}
@Override
public String getName() {
return "multiplyInt";
}
public String getPath() {
return path;
}
@Override
public Object change(MutableServerEntry entry) {
int oldValue = (Integer) entry.getPathValue(path);
int newValue = oldValue * 20;
entry.setPathValue(path, newValue);
return newValue;
}
}
GigaSpace gigaSpace = new GigaSpaceConfigurer(new SpaceProxyConfigurer("xapSpace")).gigaSpace();
SQLQuery<Employee> query = new SQLQuery<Employee>(Employee.class, "salary > 50");
ChangeResult<Employee> result = gigaSpace.change(query,
new ChangeSet().custom(new MultiplyIntegerChangeOperation("salary")),
ChangeModifiers.RETURN_DETAILED_RESULTS);