/*
 * 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.lucene.store;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NoSuchFileException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.zip.CRC32;

import org.apache.lucene.index.IndexFileNames;
import org.apache.lucene.util.BitUtil;

A ByteBuffer-based Directory implementation that can be used to store index files on the heap.

Important: Note that MMapDirectory is nearly always a better choice as it uses OS caches more effectively (through memory-mapped buffers). A heap-based directory like this one can have the advantage in case of ephemeral, small, short-lived indexes when disk syncs provide an additional overhead.

@lucene.experimental
/** * A {@link ByteBuffer}-based {@link Directory} implementation that * can be used to store index files on the heap. * * <p>Important: Note that {@link MMapDirectory} is nearly always a better choice as * it uses OS caches more effectively (through memory-mapped buffers). * A heap-based directory like this one can have the advantage in case of ephemeral, small, * short-lived indexes when disk syncs provide an additional overhead.</p> * * @lucene.experimental */
public final class ByteBuffersDirectory extends BaseDirectory { public static final BiFunction<String, ByteBuffersDataOutput, IndexInput> OUTPUT_AS_MANY_BUFFERS = (fileName, output) -> { ByteBuffersDataInput dataInput = output.toDataInput(); String inputName = String.format(Locale.ROOT, "%s (file=%s, buffers=%s)", ByteBuffersIndexInput.class.getSimpleName(), fileName, dataInput.toString()); return new ByteBuffersIndexInput(dataInput, inputName); }; public static final BiFunction<String, ByteBuffersDataOutput, IndexInput> OUTPUT_AS_ONE_BUFFER = (fileName, output) -> { ByteBuffersDataInput dataInput = new ByteBuffersDataInput(Arrays.asList(ByteBuffer.wrap(output.toArrayCopy()))); String inputName = String.format(Locale.ROOT, "%s (file=%s, buffers=%s)", ByteBuffersIndexInput.class.getSimpleName(), fileName, dataInput.toString()); return new ByteBuffersIndexInput(dataInput, inputName); }; public static final BiFunction<String, ByteBuffersDataOutput, IndexInput> OUTPUT_AS_BYTE_ARRAY = OUTPUT_AS_ONE_BUFFER; public static final BiFunction<String, ByteBuffersDataOutput, IndexInput> OUTPUT_AS_MANY_BUFFERS_LUCENE = (fileName, output) -> { List<ByteBuffer> bufferList = output.toBufferList(); bufferList.add(ByteBuffer.allocate(0)); int chunkSizePower; int blockSize = ByteBuffersDataInput.determineBlockPage(bufferList); if (blockSize == 0) { chunkSizePower = 30; } else { chunkSizePower = Integer.numberOfTrailingZeros(BitUtil.nextHighestPowerOfTwo(blockSize)); } String inputName = String.format(Locale.ROOT, "%s (file=%s)", ByteBuffersDirectory.class.getSimpleName(), fileName); ByteBufferGuard guard = new ByteBufferGuard("none", (String resourceDescription, ByteBuffer b) -> {}); return ByteBufferIndexInput.newInstance(inputName, bufferList.toArray(new ByteBuffer [bufferList.size()]), output.size(), chunkSizePower, guard); }; private final Function<String, String> tempFileName = new Function<String, String>() { private final AtomicLong counter = new AtomicLong(); @Override public String apply(String suffix) { return suffix + "_" + Long.toString(counter.getAndIncrement(), Character.MAX_RADIX); } }; private final ConcurrentHashMap<String, FileEntry> files = new ConcurrentHashMap<>();
Conversion between a buffered index output and the corresponding index input for a given file.
/** * Conversion between a buffered index output and the corresponding index input * for a given file. */
private final BiFunction<String, ByteBuffersDataOutput, IndexInput> outputToInput;
A supplier of ByteBuffersDataOutput instances used to buffer up the content of written files.
/** * A supplier of {@link ByteBuffersDataOutput} instances used to buffer up * the content of written files. */
private final Supplier<ByteBuffersDataOutput> bbOutputSupplier; public ByteBuffersDirectory() { this(new SingleInstanceLockFactory()); } public ByteBuffersDirectory(LockFactory lockFactory) { this(lockFactory, ByteBuffersDataOutput::new, OUTPUT_AS_MANY_BUFFERS); } public ByteBuffersDirectory(LockFactory factory, Supplier<ByteBuffersDataOutput> bbOutputSupplier, BiFunction<String, ByteBuffersDataOutput, IndexInput> outputToInput) { super(factory); this.outputToInput = Objects.requireNonNull(outputToInput); this.bbOutputSupplier = Objects.requireNonNull(bbOutputSupplier); } @Override public String[] listAll() throws IOException { ensureOpen(); return files.keySet().stream().sorted().toArray(String[]::new); } @Override public void deleteFile(String name) throws IOException { ensureOpen(); FileEntry removed = files.remove(name); if (removed == null) { throw new NoSuchFileException(name); } } @Override public long fileLength(String name) throws IOException { ensureOpen(); FileEntry file = files.get(name); if (file == null) { throw new NoSuchFileException(name); } return file.length(); } @Override public IndexOutput createOutput(String name, IOContext context) throws IOException { ensureOpen(); FileEntry e = new FileEntry(name); if (files.putIfAbsent(name, e) != null) { throw new FileAlreadyExistsException("File already exists: " + name); } return e.createOutput(outputToInput); } @Override public IndexOutput createTempOutput(String prefix, String suffix, IOContext context) throws IOException { ensureOpen(); while (true) { String name = IndexFileNames.segmentFileName(prefix, tempFileName.apply(suffix), "tmp"); FileEntry e = new FileEntry(name); if (files.putIfAbsent(name, e) == null) { return e.createOutput(outputToInput); } } } @Override public void rename(String source, String dest) throws IOException { ensureOpen(); FileEntry file = files.get(source); if (file == null) { throw new NoSuchFileException(source); } if (files.putIfAbsent(dest, file) != null) { throw new FileAlreadyExistsException(dest); } if (!files.remove(source, file)) { throw new IllegalStateException("File was unexpectedly replaced: " + source); } files.remove(source); } @Override public void sync(Collection<String> names) throws IOException { ensureOpen(); } @Override public void syncMetaData() throws IOException { ensureOpen(); } @Override public IndexInput openInput(String name, IOContext context) throws IOException { ensureOpen(); FileEntry e = files.get(name); if (e == null) { throw new NoSuchFileException(name); } else { return e.openInput(); } } @Override public void close() throws IOException { isOpen = false; files.clear(); } @Override public Set<String> getPendingDeletions() { return Collections.emptySet(); } private final class FileEntry { private final String fileName; private volatile IndexInput content; private volatile long cachedLength; public FileEntry(String name) { this.fileName = name; } public long length() { // We return 0 length until the IndexOutput is closed and flushed. return cachedLength; } public IndexInput openInput() throws IOException { IndexInput local = this.content; if (local == null) { throw new AccessDeniedException("Can't open a file still open for writing: " + fileName); } return local.clone(); } final IndexOutput createOutput(BiFunction<String, ByteBuffersDataOutput, IndexInput> outputToInput) throws IOException { if (content != null) { throw new IOException("Can only write to a file once: " + fileName); } String clazzName = ByteBuffersDirectory.class.getSimpleName(); String outputName = String.format(Locale.ROOT, "%s output (file=%s)", clazzName, fileName); return new ByteBuffersIndexOutput( bbOutputSupplier.get(), outputName, fileName, new CRC32(), (output) -> { content = outputToInput.apply(fileName, output); cachedLength = output.size(); }); } } }