Skip to content

Latest commit

 

History

History
164 lines (128 loc) · 5.75 KB

CM_Tutorial_DataAccess.adoc

File metadata and controls

164 lines (128 loc) · 5.75 KB

DataAccess and SizedReader

This pair of interfaces is configured using ChronicleMapBuilder.keyReaderAndDataAccess() or valueReaderAndDataAccess() for the key, or value type, of the map respectively.

The reader part, SizedReader, is the same as in SizedWriter and SizedReader pair. DataAccess is an "advanced" interface to replace SizedWriter.

The main method in DataAccess is Data<T> getData(@NotNull T instance). It returns a Data accessor which is used to write a "serialized" form of the instance to off-heap memory. Data.size() on the returned Data object is used for the same purpose as the SizedWriter.size() method in the SizedWriter and SizedReader pair interfaces. Data.writeTo() is used instead of SizedWriter.write().

DataAccess assumes that the Data object, returned from the getData() method is cached in some way. That is why it also has an uninit() method to clear references to the serialized object after a query operation to a Chronicle Map is complete to prevent memory leaks. This, in turn, implies that the DataAccess implementation is stateful. Therefore, DataAccess is made a sub-interface of StatefulCopyable to force all DataAccess implementations to implement StatefulCopyable as well.

See Understanding StatefulCopyable for more information on this.

If your DataAccess implementation is not actually stateful, it is free to return this from the StatefulCopyable.copy() method.

The DataAccess interface is primarily intended for "serializing" objects that are already sequences of bytes, and in fact do not require serialization; for example, byte[], ByteBuffer, arrays of Java primitives. For such types of objects, DataAccess allows bypassing of the intermediate buffering, copying data directly from objects to Chronicle Map’s off-heap memory.

For example, look at the DataAccess implementation for byte[]:

public class ByteArrayDataAccess extends AbstractData<byte[]> implements DataAccess<byte[]> {

    /**
     * Cache field
     */
    private transient BytesStore<?, ?> bs;

    /**
     * State field
     */
    private transient byte[] array;

    public ByteArrayDataAccess() {
        initTransients();
    }

    private void initTransients() {
        bs = null;
    }

    @Override
    public RandomDataInput bytes() {
        return bs;
    }

    @Override
    public long offset() {
        return bs.start();
    }

    @Override
    public long size() {
        return bs.capacity();
    }

    @Override
    public byte[] get() {
        return array;
    }

    @Override
    public byte[] getUsing(@Nullable byte[] using) {
        if (using == null || using.length != array.length)
            using = new byte[array.length];
        System.arraycopy(array, 0, using, 0, array.length);
        return using;
    }

    @Override
    public Data<byte[]> getData(@NotNull byte[] instance) {
        array = instance;
        bs = BytesStore.wrap(array);
        return this;
    }

    @Override
    public void uninit() {
        array = null;
        bs = null;
    }

    @Override
    public DataAccess<byte[]> copy() {
        return new ByteArrayDataAccess();
    }

    @Override
    public void writeMarshallable(@NotNull WireOut wireOut) {
        // no fields to write
    }

    @Override
    public void readMarshallable(@NotNull WireIn wireIn) {
        // no fields to read
        initTransients();
    }

    @Override
    public String toString() {
        return new String(array, StandardCharsets.UTF_8);
    }
}

The getData() method returns this, and the DataAccess implementation implements the Data interface as well. This is recommended practice, because it reduces the number of objects involved (hence pointer chasing), and keeps DataAccess, and Data logic together.

The Data interface puts constraints on equals(), hashCode(), and toString() implementations. This is why ByteArrayDataAccess sub-classes AbstractData, and inherits proper implementations from it. A serializer strategy implementation can have equals(), hashCode(), and toString() from a very different domain, because those methods are never called on serializers inside Chronicle Map.

The easiest way to implement equals(), hashCode(), and toString() is to extend the AbstractData class. If it is not possible (perhaps the Data implementation already extends some other class), do this by delegating to dataEquals(), dataHashCode(), and dataToString() default methods, provided in the Data interface.

Corresponding SizedReader for byte[]:

public final class ByteArraySizedReader
        implements SizedReader<byte[]>, EnumMarshallable<ByteArraySizedReader> {

    public static final ByteArraySizedReader INSTANCE = new ByteArraySizedReader();

    private ByteArraySizedReader() {
    }

    @NotNull
    @Override
    public byte[] read(@NotNull Bytes in, long size, @Nullable byte[] using) {
        if (size < 0L || size > (long) Integer.MAX_VALUE) {
            throw new IORuntimeException("byte[] size should be non-negative int, " +
                    size + " given. Memory corruption?");
        }
        int arrayLength = (int) size;
        if (using == null || arrayLength != using.length)
            using = new byte[arrayLength];
        in.read(using);
        return using;
    }

    @NotNull
    @Override
    public ByteArraySizedReader readResolve() {
        return INSTANCE;
    }
}
Note
If you configure byte[] key, or value type, then this pair of serializers is used as the default.