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

com.swirlds.merkledb.files.DataFileWriter Maven / Gradle / Ivy

/*
 * Copyright (C) 2023-2024 Hedera Hashgraph, LLC
 *
 * Licensed 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 com.swirlds.merkledb.files;

import static com.swirlds.merkledb.files.DataFileCommon.FIELD_DATAFILE_ITEMS;
import static com.swirlds.merkledb.files.DataFileCommon.PAGE_SIZE;
import static com.swirlds.merkledb.files.DataFileCommon.createDataFilePath;

import com.hedera.pbj.runtime.ProtoWriterTools;
import com.hedera.pbj.runtime.io.buffer.BufferedData;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.function.Consumer;

/**
 * Writer for creating a data file. A data file contains a number of data items. Each data item can
 * be variable or fixed size and is considered as a black box. All access to contents of the data
 * item is done via the BaseSerializer.
 *
 * 

This is designed to be used from a single thread. * *

At the end of the file it is padded till a 4096 byte page boundary then a footer page is * written by DataFileMetadata. * *

Protobuf schema: see {@link DataFileReader} for details. */ public final class DataFileWriter { /** Mapped buffer size */ private static final int MMAP_BUF_SIZE = PAGE_SIZE * 1024 * 64; /** * The current mapped byte buffer used for writing. When overflowed, it is released, and another * buffer is mapped from the file channel. */ private MappedByteBuffer writingMmap; /** * Offset, in bytes, of the current mapped byte buffer in the file channel. After the file is * completely written and closed, this field value is equal to the file size. */ private long mmapPositionInFile = 0; /* */ private BufferedData writingPbjData; private MappedByteBuffer writingHeaderMmap; private BufferedData writingHeaderPbjData; /** The path to the data file we are writing */ private final Path path; /** File metadata */ private final DataFileMetadata metadata; /** * Count of the number of data items we have written so far. Ready to be stored in footer * metadata */ private long dataItemCount = 0; /** * Create a new data file in the given directory, in append mode. Puts the object into "writing" * mode (i.e. creates a lock file. So you'd better start writing data and be sure to finish it * off). * * @param filePrefix string prefix for all files, must not contain "_" chars * @param dataFileDir the path to directory to create the data file in * @param index the index number for this file * @param creationTime the time stamp for the creation time for this file */ public DataFileWriter( final String filePrefix, final Path dataFileDir, final int index, final Instant creationTime, final int compactionLevel) throws IOException { this.path = createDataFilePath(filePrefix, dataFileDir, index, creationTime, DataFileCommon.FILE_EXTENSION); metadata = new DataFileMetadata( 0, // data item count will be updated later in finishWriting() index, creationTime, compactionLevel); Files.createFile(path); writeHeader(); } /** * Maps the writing byte buffer to the given position in the file. Byte buffer size is always * {@link #MMAP_BUF_SIZE}. Previous mapped byte buffer, if not null, is released. * * @param newMmapPos new mapped byte buffer position in the file, in bytes * @throws IOException if I/O error(s) occurred */ private void moveWritingBuffer(final long newMmapPos) throws IOException { try (final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE)) { final MappedByteBuffer newMmap = channel.map(MapMode.READ_WRITE, newMmapPos, MMAP_BUF_SIZE); if (newMmap == null) { throw new IOException("Failed to map file channel to memory"); } if (writingMmap != null) { DataFileCommon.closeMmapBuffer(writingMmap); } mmapPositionInFile = newMmapPos; writingMmap = newMmap; writingPbjData = BufferedData.wrap(writingMmap); } } private void writeHeader() throws IOException { try (final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE)) { writingHeaderMmap = channel.map(MapMode.READ_WRITE, 0, 1024); writingHeaderPbjData = BufferedData.wrap(writingHeaderMmap); metadata.writeTo(writingHeaderPbjData); } // prepare to write data items moveWritingBuffer(writingHeaderPbjData.position()); } /** * Get the path for the file being written. Useful when needing to get a reader to the file. * * @return file path */ public Path getPath() { return path; } /** * Get file metadata for the written file. * * @return data file metadata */ public DataFileMetadata getMetadata() { return metadata; } /** * Store data item in file returning location it was stored at. * * @param dataItem the data item to write * @return the data location of written data in bytes * @throws IOException if there was a problem appending data to file */ public synchronized long storeDataItem(final BufferedData dataItem) throws IOException { // find offset for the start of this new data item, we assume we always write data in a // whole number of blocks final long currentWritingMmapPos = writingPbjData.position(); final long byteOffset = mmapPositionInFile + currentWritingMmapPos; // write serialized data final int size = Math.toIntExact(dataItem.remaining()); if (writingPbjData.remaining() < ProtoWriterTools.sizeOfDelimited(FIELD_DATAFILE_ITEMS, size)) { moveWritingBuffer(byteOffset); } try { ProtoWriterTools.writeDelimited(writingPbjData, FIELD_DATAFILE_ITEMS, size, o -> o.writeBytes(dataItem)); } catch (final BufferOverflowException e) { // Buffer overflow here means the mapped buffer is smaller than even a single data item throw new IOException(DataFileCommon.ERROR_DATAITEM_TOO_LARGE, e); } // increment data item counter dataItemCount++; // return the offset where we wrote the data return DataFileCommon.dataLocation(metadata.getIndex(), byteOffset); } /** * Store data item in file returning location it was stored at. * * @param dataItemWriter the data item to write * @param dataItemSize the data item size, in bytes * @return the data location of written data in bytes * @throws IOException if there was a problem appending data to file */ public synchronized long storeDataItem(final Consumer dataItemWriter, final int dataItemSize) throws IOException { // find offset for the start of this new data item, we assume we always write data in a // whole number of blocks final long currentWritingMmapPos = writingPbjData.position(); final long byteOffset = mmapPositionInFile + currentWritingMmapPos; // write serialized data if (writingPbjData.remaining() < ProtoWriterTools.sizeOfDelimited(FIELD_DATAFILE_ITEMS, dataItemSize)) { moveWritingBuffer(byteOffset); } try { ProtoWriterTools.writeDelimited(writingPbjData, FIELD_DATAFILE_ITEMS, dataItemSize, dataItemWriter); } catch (final BufferOverflowException e) { // Buffer overflow here means the mapped buffer is smaller than even a single data item throw new IOException(DataFileCommon.ERROR_DATAITEM_TOO_LARGE, e); } // increment data item counter dataItemCount++; // return the offset where we wrote the data return DataFileCommon.dataLocation(metadata.getIndex(), byteOffset); } /** * When you finished append to a new file, call this to seal the file and make it read only for * reading. * * @throws IOException if there was a problem sealing file or opening again as read only */ public synchronized void finishWriting() throws IOException { // total file size is where the current writing pos is final long totalFileSize = mmapPositionInFile + writingPbjData.position(); // update data item count in the metadata and in the file // not that updateDataItemCount() messes up with writing buffer state (position), but // the buffer will be closed below anyway metadata.updateDataItemCount(writingHeaderPbjData, dataItemCount); // release all the resources DataFileCommon.closeMmapBuffer(writingHeaderMmap); DataFileCommon.closeMmapBuffer(writingMmap); try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE)) { channel.truncate(totalFileSize); // after finishWriting(), mmapPositionInFile should be equal to the file size mmapPositionInFile = totalFileSize; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy