Separate out RaftEntryMeta
[controller.git] / atomix-storage / src / main / java / io / atomix / storage / journal / JournalSegmentFile.java
1 /*
2  * Copyright 2015-2022 Open Networking Foundation and others.  All rights reserved.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package io.atomix.storage.journal;
17
18 import static java.util.Objects.requireNonNull;
19
20 import com.google.common.base.MoreObjects;
21 import io.netty.buffer.ByteBufAllocator;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.RandomAccessFile;
25 import java.nio.channels.FileChannel;
26 import java.nio.file.Path;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29
30 /**
31  * Segment file utility.
32  *
33  * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
34  */
35 final class JournalSegmentFile {
36     private static final char PART_SEPARATOR = '-';
37     private static final char EXTENSION_SEPARATOR = '.';
38     private static final String EXTENSION = "log";
39
40     private final @NonNull JournalSegmentDescriptor descriptor;
41     private final @NonNull ByteBufAllocator allocator;
42     private final @NonNull RandomAccessFile file;
43     private final @NonNull Path path;
44
45     private JournalSegmentFile(final Path path, final ByteBufAllocator allocator,
46             final JournalSegmentDescriptor descriptor, final RandomAccessFile file) {
47         this.path = requireNonNull(path);
48         this.allocator = requireNonNull(allocator);
49         this.descriptor = requireNonNull(descriptor);
50         this.file = requireNonNull(file);
51     }
52
53     static @NonNull JournalSegmentFile createNew(final String name, final File directory,
54             final ByteBufAllocator allocator, final JournalSegmentDescriptor descriptor) throws IOException {
55         final var file = createSegmentFile(name, directory, descriptor.id());
56         final var raf = new RandomAccessFile(file, "rw");
57         try {
58             raf.setLength(descriptor.maxSegmentSize());
59             raf.write(descriptor.toArray());
60         } catch (IOException e) {
61             raf.close();
62             throw e;
63         }
64         return new JournalSegmentFile(file.toPath(), allocator, descriptor, raf);
65     }
66
67     static @NonNull JournalSegmentFile openExisting(final Path path, final ByteBufAllocator allocator)
68             throws IOException {
69         final var raf = new RandomAccessFile(path.toFile(), "rw");
70         final JournalSegmentDescriptor descriptor;
71         try {
72             // read the descriptor
73             descriptor = JournalSegmentDescriptor.readFrom(raf.getChannel());
74         } catch (IOException e) {
75             raf.close();
76             throw e;
77         }
78         return new JournalSegmentFile(path, allocator, descriptor, raf);
79     }
80
81     /**
82      * Returns the segment file path.
83      *
84      * @return The segment file path
85      */
86     @NonNull Path path() {
87         return path;
88     }
89
90     /**
91      * Returns the {@link ByteBufAllocator} for this file.
92      *
93      * @return A {@link ByteBufAllocator}
94      */
95     @NonNull ByteBufAllocator allocator() {
96         return allocator;
97     }
98
99     /**
100      * Returns the segment version.
101      *
102      * @return the segment version
103      */
104     int version() {
105         return descriptor.version();
106     }
107
108     /**
109      * Returns the segment identifier.
110      *
111      * @return the segment identifier
112      */
113     long segmentId() {
114         return descriptor.id();
115     }
116
117     /**
118      * Returns the index of first entry stored in this file.
119      *
120      * @return the index of first entry stored in this file
121      */
122     long firstIndex() {
123         return descriptor.index();
124     }
125
126     int maxSize() {
127         return descriptor.maxSegmentSize();
128     }
129
130     int size() throws IOException {
131         return (int) file.length();
132     }
133
134     FileChannel channel() {
135         return file.getChannel();
136     }
137
138     /**
139      * Access this file using specified {@link StorageLevel} and maximum entry size.
140      *
141      * @param level a {@link StorageLevel}
142      * @param maxEntrySize maximum size of stored entry
143      * @return A {@link MappedFileAccess}
144      * @throws IOException if an I/O error occurs
145      */
146     @NonNullByDefault
147     FileAccess newAccess(final StorageLevel level, final int maxEntrySize) throws IOException {
148         return switch (level) {
149             case DISK -> new DiskFileAccess(this, maxEntrySize);
150             case MAPPED -> MappedFileAccess.of(this, maxEntrySize);
151         };
152     }
153
154     void close() throws IOException {
155         file.close();
156     }
157
158     @Override
159     public String toString() {
160         return MoreObjects.toStringHelper(this).add("path", path).add("descriptor", descriptor).toString();
161     }
162
163     /**
164      * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
165      *
166      * @throws NullPointerException if {@code file} is null
167      */
168     public static boolean isSegmentFile(final String name, final File file) {
169         return isSegmentFile(name, file.getName());
170     }
171
172     /**
173      * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
174      *
175      * @param journalName the name of the journal
176      * @param fileName the name of the file to check
177      * @throws NullPointerException if {@code file} is null
178      */
179     public static boolean isSegmentFile(final String journalName, final String fileName) {
180         requireNonNull(journalName, "journalName cannot be null");
181         requireNonNull(fileName, "fileName cannot be null");
182
183         int partSeparator = fileName.lastIndexOf(PART_SEPARATOR);
184         int extensionSeparator = fileName.lastIndexOf(EXTENSION_SEPARATOR);
185
186         if (extensionSeparator == -1 || partSeparator == -1 || extensionSeparator < partSeparator
187             || !fileName.endsWith(EXTENSION)) {
188             return false;
189         }
190
191         for (int i = partSeparator + 1; i < extensionSeparator; i++) {
192             if (!Character.isDigit(fileName.charAt(i))) {
193                 return false;
194             }
195         }
196
197         return fileName.startsWith(journalName);
198     }
199
200     /**
201      * Creates a segment file for the given directory, log name, segment ID, and segment version.
202      */
203     static File createSegmentFile(final String name, final File directory, final long id) {
204         return new File(directory, String.format("%s-%d.log", requireNonNull(name, "name cannot be null"), id));
205     }
206 }