2 * Copyright 2015-2022 Open Networking Foundation and others. All rights reserved.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package io.atomix.storage.journal;
18 import static java.util.Objects.requireNonNull;
20 import com.google.common.base.MoreObjects;
22 import java.io.IOException;
23 import java.io.RandomAccessFile;
24 import java.nio.ByteBuffer;
25 import java.nio.MappedByteBuffer;
26 import java.nio.channels.FileChannel;
27 import java.nio.channels.FileChannel.MapMode;
28 import java.nio.file.Path;
29 import org.eclipse.jdt.annotation.NonNull;
32 * Segment file utility.
34 * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
36 final class JournalSegmentFile {
37 private static final char PART_SEPARATOR = '-';
38 private static final char EXTENSION_SEPARATOR = '.';
39 private static final String EXTENSION = "log";
41 * Just do not bother with IO smaller than this many bytes.
43 private static final int MIN_IO_SIZE = 8192;
45 private final @NonNull JournalSegmentDescriptor descriptor;
46 private final @NonNull Path path;
48 private final RandomAccessFile file;
50 private JournalSegmentFile(final Path path, final JournalSegmentDescriptor descriptor,
51 final RandomAccessFile file) {
52 this.path = requireNonNull(path);
53 this.descriptor = requireNonNull(descriptor);
54 this.file = requireNonNull(file);
57 static @NonNull JournalSegmentFile createNew(final String name, final File directory,
58 final JournalSegmentDescriptor descriptor) throws IOException {
59 final var file = createSegmentFile(name, directory, descriptor.id());
60 final var raf = new RandomAccessFile(file, "rw");
62 raf.setLength(descriptor.maxSegmentSize());
63 raf.write(descriptor.toArray());
64 } catch (IOException e) {
68 return new JournalSegmentFile(file.toPath(), descriptor, raf);
71 static @NonNull JournalSegmentFile openExisting(final Path path) throws IOException {
72 final var raf = new RandomAccessFile(path.toFile(), "rw");
73 final JournalSegmentDescriptor descriptor;
75 // read the descriptor
76 descriptor = JournalSegmentDescriptor.readFrom(raf.getChannel());
77 } catch (IOException e) {
81 return new JournalSegmentFile(path, descriptor, raf);
85 * Returns the segment file.
87 * @return The segment file.
89 @NonNull Path path() {
94 * Returns the segment version.
96 * @return the segment version
99 return descriptor.version();
103 * Returns the segment identifier.
105 * @return the segment identifier
108 return descriptor.id();
112 * Returns the index of first entry stored in this file.
114 * @return the index of first entry stored in this file
117 return descriptor.index();
121 return descriptor.maxSegmentSize();
124 int size() throws IOException {
125 return (int) file.length();
128 FileChannel channel() {
129 return file.getChannel();
133 * Map the contents of the file into memory.
135 * @return A {@link MappedByteBuffer}
136 * @throws IOException if an I/O error occurs
138 @NonNull MappedByteBuffer map() throws IOException {
139 return channel().map(MapMode.READ_WRITE, 0, maxSize());
142 void close() throws IOException {
146 ByteBuffer allocateBuffer(final int maxEntrySize) {
147 return ByteBuffer.allocate(chooseBufferSize(maxEntrySize));
151 public String toString() {
152 return MoreObjects.toStringHelper(this).add("path", path).add("descriptor", descriptor).toString();
155 private int chooseBufferSize(final int maxEntrySize) {
156 final int maxSegmentSize = maxSize();
157 if (maxSegmentSize <= MIN_IO_SIZE) {
158 // just buffer the entire segment
159 return maxSegmentSize;
162 // one full entry plus its header, or MIN_IO_SIZE, which benefits the read of many small entries
163 final int minBufferSize = maxEntrySize + SegmentEntry.HEADER_BYTES;
164 return minBufferSize <= MIN_IO_SIZE ? MIN_IO_SIZE : minBufferSize;
168 * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
170 * @throws NullPointerException if {@code file} is null
172 public static boolean isSegmentFile(final String name, final File file) {
173 return isSegmentFile(name, file.getName());
177 * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
179 * @param journalName the name of the journal
180 * @param fileName the name of the file to check
181 * @throws NullPointerException if {@code file} is null
183 public static boolean isSegmentFile(final String journalName, final String fileName) {
184 requireNonNull(journalName, "journalName cannot be null");
185 requireNonNull(fileName, "fileName cannot be null");
187 int partSeparator = fileName.lastIndexOf(PART_SEPARATOR);
188 int extensionSeparator = fileName.lastIndexOf(EXTENSION_SEPARATOR);
190 if (extensionSeparator == -1 || partSeparator == -1 || extensionSeparator < partSeparator
191 || !fileName.endsWith(EXTENSION)) {
195 for (int i = partSeparator + 1; i < extensionSeparator; i++) {
196 if (!Character.isDigit(fileName.charAt(i))) {
201 return fileName.startsWith(journalName);
205 * Creates a segment file for the given directory, log name, segment ID, and segment version.
207 static File createSegmentFile(final String name, final File directory, final long id) {
208 return new File(directory, String.format("%s-%d.log", requireNonNull(name, "name cannot be null"), id));