All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.druid.segment.data.GenericIndexed Maven / Gradle / Ivy

There is a newer version: 30.0.1
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.druid.segment.data;

import com.google.common.primitives.Ints;
import org.apache.druid.collections.ResourceHolder;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.common.utils.SerializerUtils;
import org.apache.druid.io.Channels;
import org.apache.druid.java.util.common.ByteBufferUtils;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.guava.Comparators;
import org.apache.druid.java.util.common.io.Closer;
import org.apache.druid.java.util.common.io.smoosh.FileSmoosher;
import org.apache.druid.java.util.common.io.smoosh.SmooshedFileMapper;
import org.apache.druid.query.monomorphicprocessing.RuntimeShapeInspector;
import org.apache.druid.segment.serde.MetaSerdeHelper;
import org.apache.druid.segment.serde.Serializer;
import org.apache.druid.segment.writeout.HeapByteBufferWriteOutBytes;
import org.apache.druid.utils.CloseableUtils;

import javax.annotation.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
import java.util.Iterator;

/**
 * A generic, flat storage mechanism.  Use static methods fromArray() or fromIterable() to construct.  If input
 * is sorted, supports binary search index lookups.  If input is not sorted, only supports array-like index lookups.
 * 

* V1 Storage Format: *

* byte 1: version (0x1) * byte 2 == 0x1 =>; allowReverseLookup * bytes 3-6 =>; numBytesUsed * bytes 7-10 =>; numElements * bytes 10-((numElements * 4) + 10): integers representing *end* offsets of byte serialized values * bytes ((numElements * 4) + 10)-(numBytesUsed + 2): 4-byte integer representing length of value, followed by bytes * for value. Length of value stored has no meaning, if next offset is strictly greater than the current offset, * and if they are the same, -1 at this field means null, and 0 at this field means some object * (potentially non-null - e. g. in the string case, that is serialized as an empty sequence of bytes). *

* V2 Storage Format * Meta, header and value files are separate and header file stored in native endian byte order. * Meta File: * byte 1: version (0x2) * byte 2 == 0x1 =>; allowReverseLookup * bytes 3-6: numberOfElementsPerValueFile expressed as power of 2. That means all the value files contains same * number of items except last value file and may have fewer elements. * bytes 7-10 =>; numElements * bytes 11-14 =>; columnNameLength * bytes 15-columnNameLength =>; columnName *

* Header file name is identified as: StringUtils.format("%s_header", columnName) * value files are identified as: StringUtils.format("%s_value_%d", columnName, fileNumber) * number of value files == numElements/numberOfElementsPerValueFile * * The version {@link EncodedStringDictionaryWriter#VERSION} is reserved and must never be specified as the * {@link GenericIndexed} version byte, else it will interfere with string column deserialization. * * @see GenericIndexedWriter */ public abstract class GenericIndexed implements CloseableIndexed, Serializer { static final byte VERSION_ONE = 0x1; static final byte VERSION_TWO = 0x2; static final byte REVERSE_LOOKUP_ALLOWED = 0x1; static final byte REVERSE_LOOKUP_DISALLOWED = 0x0; static final int NULL_VALUE_SIZE_MARKER = -1; private static final SerializerUtils SERIALIZER_UTILS = new SerializerUtils(); /** * An ObjectStrategy that returns a big-endian ByteBuffer pointing to original data. * * The returned ByteBuffer is a fresh read-only instance, so it is OK for callers to modify its position, limit, etc. * However, it does point to the original data, so callers must take care not to use it if the original data may * have been freed. * * The compare method of this instance uses {@link StringUtils#compareUtf8UsingJavaStringOrdering(byte[], byte[])} * so that behavior is consistent with {@link #STRING_STRATEGY}. */ public static final ObjectStrategy UTF8_STRATEGY = new ObjectStrategy() { @Override public Class getClazz() { return ByteBuffer.class; } @Override public ByteBuffer fromByteBuffer(final ByteBuffer buffer, final int numBytes) { final ByteBuffer dup = buffer.asReadOnlyBuffer(); dup.limit(buffer.position() + numBytes); return dup; } @Override @Nullable public byte[] toBytes(@Nullable ByteBuffer buf) { if (buf == null) { return null; } // This method doesn't have javadocs and I'm not sure if it is OK to modify the "val" argument. Copy defensively. final ByteBuffer dup = buf.duplicate(); final byte[] bytes = new byte[dup.remaining()]; dup.get(bytes); return bytes; } @Override public int compare(@Nullable ByteBuffer o1, @Nullable ByteBuffer o2) { return ByteBufferUtils.utf8Comparator().compare(o1, o2); } }; public static final ObjectStrategy STRING_STRATEGY = new ObjectStrategy() { @Override public Class getClazz() { return String.class; } @Override public String fromByteBuffer(final ByteBuffer buffer, final int numBytes) { return StringUtils.fromUtf8(buffer, numBytes); } @Override @Nullable public byte[] toBytes(@Nullable String val) { return StringUtils.toUtf8Nullable(NullHandling.nullToEmptyIfNeeded(val)); } @Override public int compare(String o1, String o2) { return Comparators.naturalNullsFirst().compare(o1, o2); } }; public static GenericIndexed read(ByteBuffer buffer, ObjectStrategy strategy) { byte versionFromBuffer = buffer.get(); if (VERSION_ONE == versionFromBuffer) { return createGenericIndexedVersionOne(buffer, strategy); } else if (VERSION_TWO == versionFromBuffer) { throw new IAE( "use read(ByteBuffer buffer, ObjectStrategy strategy, SmooshedFileMapper fileMapper)" + " to read version 2 indexed." ); } throw new IAE("Unknown version[%d]", (int) versionFromBuffer); } public static GenericIndexed read(ByteBuffer buffer, ObjectStrategy strategy, SmooshedFileMapper fileMapper) { byte versionFromBuffer = buffer.get(); if (VERSION_ONE == versionFromBuffer) { return createGenericIndexedVersionOne(buffer, strategy); } else if (VERSION_TWO == versionFromBuffer) { return createGenericIndexedVersionTwo(buffer, strategy, fileMapper); } throw new IAE("Unknown version [%s]", versionFromBuffer); } public static GenericIndexed fromArray(T[] objects, ObjectStrategy strategy) { return fromIterable(Arrays.asList(objects), strategy); } public static GenericIndexed> ofCompressedByteBuffers( Iterable buffers, CompressionStrategy compression, int bufferSize, ByteOrder order, Closer closer ) { return fromIterableVersionOne( buffers, GenericIndexedWriter.compressedByteBuffersWriteObjectStrategy(compression, bufferSize, closer), false, DecompressingByteBufferObjectStrategy.of(order, compression) ); } public static GenericIndexed fromIterable(Iterable objectsIterable, ObjectStrategy strategy) { return fromIterableVersionOne(objectsIterable, strategy, strategy.canCompare(), strategy); } static int getNumberOfFilesRequired(int bagSize, long numWritten) { int numberOfFilesRequired = (int) (numWritten / bagSize); if ((numWritten % bagSize) != 0) { numberOfFilesRequired += 1; } return numberOfFilesRequired; } protected final ObjectStrategy strategy; protected final boolean allowReverseLookup; protected final int size; public GenericIndexed( final ObjectStrategy strategy, final boolean allowReverseLookup, final int size ) { this.strategy = strategy; this.allowReverseLookup = allowReverseLookup; this.size = size; } public abstract BufferIndexed singleThreaded(); @Override public abstract long getSerializedSize(); private static final class V1 extends GenericIndexed { @SuppressWarnings("rawtypes") private static final MetaSerdeHelper META_SERDE_HELPER = MetaSerdeHelper .firstWriteByte((GenericIndexed.V1 x) -> VERSION_ONE) .writeByte(x -> x.allowReverseLookup ? REVERSE_LOOKUP_ALLOWED : REVERSE_LOOKUP_DISALLOWED) .writeInt(x -> Ints.checkedCast(x.theBuffer.remaining() + (long) Integer.BYTES)) .writeInt(x -> x.size); private final ByteBuffer theBuffer; private final int headerOffset; private final int valuesOffset; V1( final ByteBuffer buffer, final ObjectStrategy strategy, final boolean allowReverseLookup ) { super(strategy, allowReverseLookup, buffer.getInt()); this.theBuffer = buffer; this.headerOffset = theBuffer.position(); this.valuesOffset = theBuffer.position() + size * Integer.BYTES; } @Nullable @Override public T get(int index) { checkIndex(index); final int startOffset; final int endOffset; if (index == 0) { startOffset = Integer.BYTES; endOffset = theBuffer.getInt(headerOffset); } else { int headerPosition = (index - 1) * Integer.BYTES; startOffset = theBuffer.getInt(headerOffset + headerPosition) + Integer.BYTES; endOffset = theBuffer.getInt(headerOffset + headerPosition + Integer.BYTES); } return copyBufferAndGet(theBuffer, valuesOffset + startOffset, valuesOffset + endOffset); } @Override public BufferIndexed singleThreaded() { final ByteBuffer copyBuffer = theBuffer.asReadOnlyBuffer(); return new BufferIndexed() { @Nullable @Override protected ByteBuffer getByteBuffer(final int index) { checkIndex(index); final int startOffset; final int endOffset; if (index == 0) { startOffset = Integer.BYTES; endOffset = theBuffer.getInt(headerOffset); } else { int headerPosition = (index - 1) * Integer.BYTES; startOffset = theBuffer.getInt(headerOffset + headerPosition) + Integer.BYTES; endOffset = theBuffer.getInt(headerOffset + headerPosition + Integer.BYTES); } return bufferedIndexedGetByteBuffer(copyBuffer, valuesOffset + startOffset, valuesOffset + endOffset); } @Override public void inspectRuntimeShape(RuntimeShapeInspector inspector) { inspector.visit("theBuffer", theBuffer); inspector.visit("copyBuffer", copyBuffer); inspector.visit("strategy", strategy); } }; } @Override public long getSerializedSize() { return META_SERDE_HELPER.size(this) + (long) theBuffer.remaining(); } @Override public void writeTo(WritableByteChannel channel, FileSmoosher smoosher) throws IOException { META_SERDE_HELPER.writeTo(channel, this); Channels.writeFully(channel, theBuffer.asReadOnlyBuffer()); } @Override public void inspectRuntimeShape(RuntimeShapeInspector inspector) { inspector.visit("theBuffer", theBuffer); inspector.visit("strategy", strategy); } } private static final class V2 extends GenericIndexed { private final ByteBuffer headerBuffer; private final ByteBuffer[] valueBuffers; private final int logBaseTwoOfElementsPerValueFile; private final int relativeIndexMask; private V2( ByteBuffer[] valueBuffs, ByteBuffer headerBuff, ObjectStrategy strategy, boolean allowReverseLookup, int logBaseTwoOfElementsPerValueFile, int numWritten ) { super(strategy, allowReverseLookup, numWritten); this.valueBuffers = valueBuffs; this.headerBuffer = headerBuff; this.logBaseTwoOfElementsPerValueFile = logBaseTwoOfElementsPerValueFile; this.relativeIndexMask = (1 << logBaseTwoOfElementsPerValueFile) - 1; headerBuffer.order(ByteOrder.nativeOrder()); } @Nullable @Override public T get(int index) { checkIndex(index); final int startOffset; final int endOffset; int relativePositionOfIndex = index & relativeIndexMask; if (relativePositionOfIndex == 0) { int headerPosition = index * Integer.BYTES; startOffset = Integer.BYTES; endOffset = headerBuffer.getInt(headerPosition); } else { int headerPosition = (index - 1) * Integer.BYTES; startOffset = headerBuffer.getInt(headerPosition) + Integer.BYTES; endOffset = headerBuffer.getInt(headerPosition + Integer.BYTES); } int fileNum = index >> logBaseTwoOfElementsPerValueFile; return copyBufferAndGet(valueBuffers[fileNum], startOffset, endOffset); } @Override public BufferIndexed singleThreaded() { final ByteBuffer[] copyValueBuffers = new ByteBuffer[valueBuffers.length]; for (int i = 0; i < valueBuffers.length; i++) { copyValueBuffers[i] = valueBuffers[i].asReadOnlyBuffer(); } return new BufferIndexed() { @Nullable @Override protected ByteBuffer getByteBuffer(int index) { checkIndex(index); final int startOffset; final int endOffset; int relativePositionOfIndex = index & relativeIndexMask; if (relativePositionOfIndex == 0) { int headerPosition = index * Integer.BYTES; startOffset = 4; endOffset = headerBuffer.getInt(headerPosition); } else { int headerPosition = (index - 1) * Integer.BYTES; startOffset = headerBuffer.getInt(headerPosition) + Integer.BYTES; endOffset = headerBuffer.getInt(headerPosition + Integer.BYTES); } int fileNum = index >> logBaseTwoOfElementsPerValueFile; return bufferedIndexedGetByteBuffer(copyValueBuffers[fileNum], startOffset, endOffset); } @Override public void inspectRuntimeShape(RuntimeShapeInspector inspector) { inspector.visit("headerBuffer", headerBuffer); // Inspecting just one example of copyValueBuffer, not needed to inspect the whole array, because all buffers // in it are the same. inspector.visit("copyValueBuffer", copyValueBuffers.length > 0 ? copyValueBuffers[0] : null); inspector.visit("strategy", strategy); } }; } @Override public long getSerializedSize() { throw new UnsupportedOperationException("Method not supported for version 2 GenericIndexed."); } @Override public void writeTo(WritableByteChannel channel, FileSmoosher smoosher) { throw new UnsupportedOperationException( "GenericIndexed serialization for V2 is unsupported. Use GenericIndexedWriter instead."); } @Override public void inspectRuntimeShape(RuntimeShapeInspector inspector) { inspector.visit("headerBuffer", headerBuffer); // Inspecting just one example of valueBuffer, not needed to inspect the whole array, because all buffers in it // are the same. inspector.visit("valueBuffer", valueBuffers.length > 0 ? valueBuffers[0] : null); inspector.visit("strategy", strategy); } } /** * Checks if {@code index} a valid `element index` in GenericIndexed. * Similar to Preconditions.checkElementIndex() except this method throws {@link IAE} with custom error message. *

* Used here to get existing behavior(same error message and exception) of V1 GenericIndexed. * * @param index index identifying an element of an GenericIndexed. */ protected void checkIndex(int index) { if (index < 0) { throw new IAE("Index[%s] < 0", index); } if (index >= size) { throw new IAE("Index[%d] >= size[%d]", index, size); } } public Class getClazz() { return strategy.getClazz(); } @Override public int size() { return size; } /** * Returns the index of "value" in this GenericIndexed object, or (-(insertion point) - 1) if the value is not * present, in the manner of Arrays.binarySearch. This strengthens the contract of Indexed, which only guarantees * that values-not-found will return some negative number. * * @param value value to search for * * @return index of value, or negative number equal to (-(insertion point) - 1). */ @Override public int indexOf(@Nullable T value) { if (!allowReverseLookup) { throw new UnsupportedOperationException("Reverse lookup not allowed."); } int minIndex = 0; int maxIndex = size - 1; while (minIndex <= maxIndex) { int currIndex = (minIndex + maxIndex) >>> 1; T currValue = get(currIndex); int comparison = strategy.compare(currValue, value); if (comparison == 0) { return currIndex; } if (comparison < 0) { minIndex = currIndex + 1; } else { maxIndex = currIndex - 1; } } return -(minIndex + 1); } @Override public boolean isSorted() { return allowReverseLookup; } @Override public Iterator iterator() { return IndexedIterable.create(this).iterator(); } @Nullable protected T copyBufferAndGet(ByteBuffer valueBuffer, int startOffset, int endOffset) { ByteBuffer copyValueBuffer = valueBuffer.asReadOnlyBuffer(); int size = endOffset - startOffset; // When size is 0 and SQL compatibility is enabled also check for null marker before returning null. // When SQL compatibility is not enabled return null for both null as well as empty string case. if (size == 0 && (NullHandling.replaceWithDefault() || copyValueBuffer.get(startOffset - Integer.BYTES) == NULL_VALUE_SIZE_MARKER)) { return null; } copyValueBuffer.position(startOffset); // fromByteBuffer must not modify the buffer limit return strategy.fromByteBuffer(copyValueBuffer, size); } /** * Single-threaded view. */ public abstract class BufferIndexed implements Indexed { @Override public int size() { return size; } @Override public T get(final int index) { final ByteBuffer buf = getByteBuffer(index); if (buf == null) { return null; } // Traditionally, ObjectStrategy.fromByteBuffer() is given a buffer with limit set to capacity, and the // actual limit is passed along as an extra parameter. final int len = buf.remaining(); buf.limit(buf.capacity()); return strategy.fromByteBuffer(buf, len); } @Nullable ByteBuffer bufferedIndexedGetByteBuffer(ByteBuffer copyValueBuffer, int startOffset, int endOffset) { int size = endOffset - startOffset; // When size is 0 and SQL compatibility is enabled also check for null marker before returning null. // When SQL compatibility is not enabled return null for both null as well as empty string case. if (size == 0 && (NullHandling.replaceWithDefault() || copyValueBuffer.get(startOffset - Integer.BYTES) == NULL_VALUE_SIZE_MARKER)) { return null; } // ObjectStrategy.fromByteBuffer() is allowed to reset the limit of the buffer. So if the limit is changed, // position() call could throw an exception, if the position is set beyond the new limit. Calling limit() // followed by position() is safe, because limit() resets position if needed. copyValueBuffer.limit(endOffset); copyValueBuffer.position(startOffset); return copyValueBuffer; } /** * Like {@link #get(int)}, but returns a {@link ByteBuffer} instead of using the {@link ObjectStrategy}. * * The returned ByteBuffer is reused by future calls. Callers must discard it before calling another method * on this BufferedIndexed object that may want to reuse the buffer. */ @Nullable protected abstract ByteBuffer getByteBuffer(int index); @Override public int indexOf(@Nullable T value) { if (!allowReverseLookup) { throw new UnsupportedOperationException("Reverse lookup not allowed."); } //noinspection ObjectEquality final boolean isByteBufferStrategy = strategy == UTF8_STRATEGY; int minIndex = 0; int maxIndex = size - 1; while (minIndex <= maxIndex) { int currIndex = (minIndex + maxIndex) >>> 1; int comparison; if (isByteBufferStrategy) { // Specialization avoids ByteBuffer allocation in strategy.fromByteBuffer. ByteBuffer currValue = getByteBuffer(currIndex); comparison = ByteBufferUtils.compareUtf8ByteBuffers(currValue, (ByteBuffer) value); } else { T currValue = get(currIndex); comparison = strategy.compare(currValue, value); } if (comparison == 0) { return currIndex; } if (comparison < 0) { minIndex = currIndex + 1; } else { maxIndex = currIndex - 1; } } return -(minIndex + 1); } @Override public boolean isSorted() { return allowReverseLookup; } @Override public Iterator iterator() { return GenericIndexed.this.iterator(); } } @Override public void close() { // nothing to close } private static GenericIndexed createGenericIndexedVersionOne(ByteBuffer byteBuffer, ObjectStrategy strategy) { boolean allowReverseLookup = byteBuffer.get() == REVERSE_LOOKUP_ALLOWED; int size = byteBuffer.getInt(); ByteBuffer bufferToUse = byteBuffer.asReadOnlyBuffer(); bufferToUse.limit(bufferToUse.position() + size); byteBuffer.position(bufferToUse.limit()); return new GenericIndexed.V1<>( bufferToUse, strategy, allowReverseLookup ); } private static GenericIndexed fromIterableVersionOne( Iterable objectsIterable, ObjectStrategy strategy, boolean allowReverseLookup, ObjectStrategy resultObjectStrategy ) { Iterator objects = objectsIterable.iterator(); if (!objects.hasNext()) { final ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES).putInt(0); buffer.flip(); return new GenericIndexed.V1<>(buffer, resultObjectStrategy, allowReverseLookup); } int count = 0; HeapByteBufferWriteOutBytes headerOut = new HeapByteBufferWriteOutBytes(); HeapByteBufferWriteOutBytes valuesOut = new HeapByteBufferWriteOutBytes(); try { T prevVal = null; do { count++; T next = objects.next(); if (allowReverseLookup && prevVal != null && !(strategy.compare(prevVal, next) < 0)) { allowReverseLookup = false; } if (next != null) { valuesOut.writeInt(0); strategy.writeTo(next, valuesOut); } else { valuesOut.writeInt(NULL_VALUE_SIZE_MARKER); } headerOut.writeInt(Ints.checkedCast(valuesOut.size())); if (prevVal instanceof Closeable) { CloseableUtils.closeAndWrapExceptions((Closeable) prevVal); } prevVal = next; } while (objects.hasNext()); if (prevVal instanceof Closeable) { CloseableUtils.closeAndWrapExceptions((Closeable) prevVal); } } catch (IOException e) { throw new RuntimeException(e); } ByteBuffer theBuffer = ByteBuffer.allocate(Ints.checkedCast(Integer.BYTES + headerOut.size() + valuesOut.size())); theBuffer.putInt(count); headerOut.writeTo(theBuffer); valuesOut.writeTo(theBuffer); theBuffer.flip(); return new GenericIndexed.V1<>(theBuffer.asReadOnlyBuffer(), resultObjectStrategy, allowReverseLookup); } private static GenericIndexed createGenericIndexedVersionTwo( ByteBuffer byteBuffer, ObjectStrategy strategy, SmooshedFileMapper fileMapper ) { if (fileMapper == null) { throw new IAE("SmooshedFileMapper can not be null for version 2."); } boolean allowReverseLookup = byteBuffer.get() == REVERSE_LOOKUP_ALLOWED; int logBaseTwoOfElementsPerValueFile = byteBuffer.getInt(); int numElements = byteBuffer.getInt(); try { String columnName = SERIALIZER_UTILS.readString(byteBuffer); int elementsPerValueFile = 1 << logBaseTwoOfElementsPerValueFile; int numberOfFilesRequired = getNumberOfFilesRequired(elementsPerValueFile, numElements); ByteBuffer[] valueBuffersToUse = new ByteBuffer[numberOfFilesRequired]; for (int i = 0; i < numberOfFilesRequired; i++) { // SmooshedFileMapper.mapFile() contract guarantees that the valueBuffer's limit equals to capacity. ByteBuffer valueBuffer = fileMapper.mapFile(GenericIndexedWriter.generateValueFileName(columnName, i)); valueBuffersToUse[i] = valueBuffer.asReadOnlyBuffer(); } ByteBuffer headerBuffer = fileMapper.mapFile(GenericIndexedWriter.generateHeaderFileName(columnName)); return new GenericIndexed.V2<>( valueBuffersToUse, headerBuffer, strategy, allowReverseLookup, logBaseTwoOfElementsPerValueFile, numElements ); } catch (IOException e) { throw new RuntimeException("File mapping failed.", e); } } }