Recall is an off-heap, allocation-free object store for the JVM.
Recall is designed for use in allocation-free or low-garbage systems. Objects are expected to be mutable in order to reduce allocation costs. For this reason, domain objects should have mutator methods for any fields that need to be serialised.
dependencies {
compile group: 'com.aitusoftware', name: 'recall-store', version: '0.2.0'
}
<dependency>
<groupId>com.aitusoftware</groupId>
<artifactId>recall-store</artifactId>
<version>0.2.0</version>
</dependency>Recall can use either a standard JDK ByteBuffer or an
AgronaUnsafeBuffer for storage of
objects outside of the Java heap.
To use the Recall object store, implement the Encoder, Decoder, and IdAccessor interface for
a given object and buffer type:
publicclassOrder {
privatelongid;
privatedoublequantity;
privatedoubleprice;
// constructor omitted// getters and setters omitted
}publicclassOrderEncoderimplementsEncoder<ByteBuffer, Order> {
publicvoidstore(ByteBufferbuffer, intoffset, Orderorder) {
buffer.putLong(offset, order.getId());
buffer.putDouble(offset + Long.BYTES, order.getQuantity());
buffer.putDouble(offset + Long.BYTES + Double.BYTES, order.getPrice());
}
}publicclassOrderDecoderimplementsDecoder<ByteBuffer, Order> {
publicvoidload(ByteBufferbuffer, intoffset, Ordertarget) {
target.setId(buffer.getLong(offset));
target.setQuantity(buffer.getDouble(offset + Long.BYTES));
target.setPrice(buffer.getDouble(offset + Long.BYTES + Double.BYTES));
}
}publicclassOrderIdAccessorimplementsIdAccessor<Order> {
publiclonggetId(Orderorder) {
returnorder.getId();
}
}Create a Store:
BufferStore<ByteBuffer> store =
newBufferStore<>(24, 100, ByteBuffer::allocateDirect, newByteBufferOps());Optionally wrap it in a SingleTypeStore (if only one type is going to be stored):
SingleTypeStore<ByteBuffer, Order> typeStore =
newSingleTypeStore<>(store, newOrderDecoder(), newOrderEncoder(),
newOrderIdAccessor());Domain objects can be serialised to off-heap storage, and retrieved at a later time:
longorderId = 42L;
OrdertestOrder = newOrder(orderId, 12.34D, 56.78D);
typeStore.store(testOrder);
Ordercontainer = newOrder(-1, -1, -1);
asserttypeStore.load(orderId, container);
assertcontainer.getQuantity() == 12.34D;Recall is able to provide efficient off-heap storage of SBE-encoded messages.
This example uses the canonical Car example from
SBE.
SBE objects must be generated with:
-Dsbe.java.generate.interfaces=true
this causes the Decoder to implement MessageDecoderFlyweight.
It is necessary to implement the IdAccessor interface for the SBE Decoder type:
publicclassCarIdAccessorimplementsIdAccessor<CarDecoder> {
publiclonggetId(CarDecoderdecoder) {
returndecoder.id();
}
}Create a SingleTypeStore for the type of the Decoder:
SingleTypeStore<UnsafeBuffer, CarDecoder> messageStore =
SbeMessageStoreFactory.forSbeMessage(newCarDecoder(),
MAX_RECORD_LENGTH, 100,
len -> newUnsafeBuffer(ByteBuffer.allocateDirect(len)),
newCarIdAccessor());Note: it is up to the application developer to determine the maximum length of any given SBE message (even in the case of variable-length fields).
If an encoded value exceeds the specified maximum record length, then the
store method will throw an IllegalArgumentException.
SBE messages can now be stored for later retrieval:
publicvoidreceiveCar(ReadableByteChannelchannel) {
CarDecoderdecoder = newCarDecoder();
UnsafeBufferbuffer = newUnsafeBuffer();
ByteBufferinputData = ByteBuffer.allocateDirect(MAX_RECORD_LENGTH);
channel.read(inputData);
inputData.flip();
buffer.wrap(inputData);
decoder.wrap(buffer, 0, BLOCK_LENGTH, VERSION);
dispatchCarReceivedEvent(decoder);
messageStore.store(decoder);
}publicvoidnotifyCarSold(longcarId) {
CarDecoderdecoder = newCarDecoder();
messageStore.load(carId, decoder);
dispatchCarSoldEvent(decoder);
}Since it is sometimes useful to be able to store and retrieve objects by something other than an integer key, Recall also provides the ability to create mappings based on variable-length keys based on either strings, or byte-sequences.
CharSequenceMap is an open-addressed hash map with that can be used to store a CharSequence
against an integer identifier.
Example usage:
privatefinalOrderByteBufferTranscodertranscoder =
newOrderByteBufferTranscoder();
privatefinalSingleTypeStore<ByteBuffer, Order> store =
newSingleTypeStore<>(
newBufferStore<>(MAX_RECORD_LENGTH, INITIAL_SIZE,
ByteBuffer::allocateDirect, newByteBufferOps()),
transcoder, transcoder, Order::getId);
privatefinalCharSequenceMaporderBySymbol =
newCharSequenceMap(MAX_KEY_LENGTH, INITIAL_SIZE, Long.MIN_VALUE);
privatevoidexecute()
{
finalString[] symbols = newString[INITIAL_SIZE];
for (inti = 0; i < INITIAL_SIZE; i++)
{
finalOrderorder = Order.of(i);
store.store(order);
orderBySymbol.insert(order.getSymbol(), order.getId());
symbols[i] = order.getSymbol().toString();
}
finalOrdercontainer = Order.of(-1L);
for (inti = 0; i < INITIAL_SIZE; i++)
{
finalStringsearchTerm = symbols[i];
finallongid = orderBySymbol.search(searchTerm);
assertThat(store.load(id, container)).isTrue();
System.out.printf("Order with symbol %s has id %d%n", searchTerm, id);
}
}ByteSequenceMap is an open-addressed hash map with that can be used to store a ByteBuffer
against an integer identifier.