Skip to content

Commit 73ecd7c

Browse files
committed
[feature] Add checksums to each journal entry. Allows us to detect a corrupted Journal
1 parent be1bf55 commit 73ecd7c

File tree

6 files changed

+93
-18
lines changed

6 files changed

+93
-18
lines changed

.classpath

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<classpathentry kind="src" path="extensions/webdav/src"/>
2424
<classpathentry kind="src" path="extensions/commands/src"/>
2525
<classpathentry kind="lib" path="lib/core/j8fu-1.21.jar"/>
26+
<classpathentry kind="lib" path="lib/core/lz4-java-1.5.0.jar"/>
2627
<classpathentry kind="lib" path="lib/core/icu4j-59_1.jar"/>
2728
<classpathentry kind="lib" path="lib/core/icu4j-localespi-59_1.jar"/>
2829
<classpathentry kind="lib" path="lib/core/pkg-java-fork.jar"/>

lib/core/lz4-java-1.5.0.jar

500 KB
Binary file not shown.

nbproject/project.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ file.reference.commons-lang3-3.7.jar=lib/optional/commons-lang3-3.7.jar
1212
file.reference.commons-net-3.6.jar=lib/optional/commons-net-3.6.jar
1313
file.reference.httpcore-4.4.8.jar=lib/optional/httpcore-4.4.8.jar
1414
file.reference.j8fu-1.21.jar=lib/core/j8fu-1.21.jar
15+
file.reference.lz4-java-1.5.0.jar=lib/core/lz4-java-1.5.0.jar
1516
file.reference.icu4j-59_1.jar=lib/core/icu4j-59_1.jar
1617
file.reference.icu4j-localespi-59_1.jar=lib/core/icu4j-localespi-59_1.jar
1718
file.reference.caffeine-2.6.2.jar=lib/core/caffeine-2.6.2.jar
@@ -369,6 +370,7 @@ javac.classpath=\
369370
${file.reference.tagsoup-1.2.1.jar}:\
370371
${file.reference.exquery-annotations-common-1.0-SNAPSHOT.jar}:\
371372
${file.reference.j8fu-1.21.jar}:\
373+
${file.reference.lz4-java-1.5.0.jar}:\
372374
${file.reference.icu4j-59_1.jar}:\
373375
${file.reference.icu4j-localespi-59_1.jar}:\
374376
${file.reference.caffeine-2.6.2.jar}:\

src/org/exist/start/start.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ lib/core/xmlrpc-server-%latest%.jar always
6565
lib/core/clj-ds-%latest%.jar always
6666
lib/core/cglib-nodep-%latest%.jar always
6767
lib/core/j8fu-%latest%.jar always
68+
lib/core/lz4-java-%latest%.jar always
6869
lib/core/icu4j-%latest%.jar always
6970
lib/core/icu4j-localespi-%latest%.jar always
7071
lib/core/caffeine-%latest%.jar always

src/org/exist/storage/journal/Journal.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.util.Optional;
3232
import java.util.stream.Stream;
3333

34+
import net.jpountz.xxhash.XXHash64;
35+
import net.jpountz.xxhash.XXHashFactory;
3436
import org.apache.logging.log4j.LogManager;
3537
import org.apache.logging.log4j.Logger;
3638
import org.exist.EXistException;
@@ -71,14 +73,15 @@
7173
* Each {@code entry} record has the format:
7274
*
7375
* <pre>{@code
74-
* [entryHeader, data, backLink]
76+
* [entryHeader, data, backLink, checksum]
7577
* }</pre>
7678
*
7779
* {@code entryHeader} 11 bytes describes the entry (see below).
7880
* {@code data} {@code entryHeader->length} bytes of data for the entry.
7981
* {@code backLink} 2 bytes (java.lang.short) offset to the start of the entry record, calculated by {@code entryHeader.length + dataLength}.
80-
* The offset for the start of the entry record can be calculated as {@code endOfRecordOffset - 2 - backLink}.
82+
* The offset for the start of the entry record can be calculated as {@code endOfRecordOffset - 8 - 2 - backLink}.
8183
* This is used when scanning the log file backwards for recovery.
84+
* {@code checksum} 8 bytes for a 64 bit checksum. The checksum includes the {@code entryHeader}, {@code data}, and {@code backLink}.
8285
*
8386
* The {@code entryHeader} has the format:
8487
*
@@ -108,7 +111,7 @@ public final class Journal {
108111
*/
109112
public static final int JOURNAL_HEADER_LEN = 6;
110113
public static final byte[] JOURNAL_MAGIC_NUMBER = {0x0E, 0x0D, 0x0B, 0x01};
111-
public static final short JOURNAL_VERSION = 2;
114+
public static final short JOURNAL_VERSION = 3;
112115

113116
public static final String RECOVERY_SYNC_ON_COMMIT_ATTRIBUTE = "sync-on-commit";
114117
public static final String RECOVERY_JOURNAL_DIR_ATTRIBUTE = "journal-dir";
@@ -135,9 +138,14 @@ public final class Journal {
135138
public static final int LOG_ENTRY_BACK_LINK_LEN = 2;
136139

137140
/**
138-
* header length + trailing back link
141+
* the length of the checkum in a log entry
139142
*/
140-
public static final int LOG_ENTRY_BASE_LEN = LOG_ENTRY_HEADER_LEN + LOG_ENTRY_BACK_LINK_LEN;
143+
public static final int LOG_ENTRY_CHECKSUM_LEN = 8;
144+
145+
/**
146+
* header length + trailing back link length + checksum length
147+
*/
148+
public static final int LOG_ENTRY_BASE_LEN = LOG_ENTRY_HEADER_LEN + LOG_ENTRY_BACK_LINK_LEN + LOG_ENTRY_CHECKSUM_LEN;
141149

142150
/**
143151
* default maximum journal size
@@ -154,6 +162,11 @@ public final class Journal {
154162
*/
155163
public static final int BUFFER_SIZE = 1024 * 1024; // bytes
156164

165+
/**
166+
* Seed used for xxhash-64 checksums calculated
167+
* by the journal.
168+
*/
169+
public static final long XXHASH64_SEED = 0x9747b28c;
157170

158171
/**
159172
* Minimum size limit for the journal file before it is replaced by a new file.
@@ -243,6 +256,8 @@ public final class Journal {
243256

244257
private volatile boolean initialised = false;
245258

259+
private final XXHash64 xxHash64 = XXHashFactory.fastestInstance().hash64();
260+
246261
public Journal(final BrokerPool pool, final Path directory) throws EXistException {
247262
this.pool = pool;
248263
this.fsJournalDir = directory.resolve("fs.journal");
@@ -343,11 +358,22 @@ public synchronized void writeToLog(final Loggable entry) throws JournalExceptio
343358
entry.setLsn(currentLsn);
344359

345360
try {
361+
final int currentBufferEntryOffset = currentBuffer.position();
362+
363+
// write entryHeader
346364
currentBuffer.put(entry.getLogType());
347365
currentBuffer.putLong(entry.getTransactionId());
348366
currentBuffer.putShort((short) size);
367+
368+
// write entry data
349369
entry.write(currentBuffer);
370+
371+
// write backlink
350372
currentBuffer.putShort((short) (size + LOG_ENTRY_HEADER_LEN));
373+
374+
// write checksum
375+
final long checksum = xxHash64.hash(currentBuffer, currentBufferEntryOffset, currentBuffer.position() - currentBufferEntryOffset, XXHASH64_SEED);
376+
currentBuffer.putLong(checksum);
351377
} catch (final BufferOverflowException e) {
352378
throw new JournalException("Buffer overflow while writing log record: " + entry.dump(), e);
353379
}
@@ -529,7 +555,7 @@ public void switchFiles() throws LogException {
529555
}
530556

531557
private void writeJournalHeader(final SeekableByteChannel channel) throws IOException {
532-
final ByteBuffer buf = ByteBuffer.allocate(JOURNAL_HEADER_LEN);
558+
final ByteBuffer buf = ByteBuffer.allocateDirect(JOURNAL_HEADER_LEN);
533559

534560
// write the magic number
535561
buf.put(JOURNAL_MAGIC_NUMBER);

src/org/exist/storage/journal/JournalReader.java

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*/
2020
package org.exist.storage.journal;
2121

22+
import net.jpountz.xxhash.StreamingXXHash64;
23+
import net.jpountz.xxhash.XXHashFactory;
2224
import org.apache.logging.log4j.LogManager;
2325
import org.apache.logging.log4j.Logger;
2426
import org.exist.storage.DBBroker;
@@ -52,6 +54,8 @@ public class JournalReader implements AutoCloseable {
5254
@Nullable
5355
private SeekableByteChannel fc;
5456

57+
private final StreamingXXHash64 xxHash64 = XXHashFactory.fastestInstance().newStreamingHash64(Journal.XXHASH64_SEED);
58+
5559
/**
5660
* Opens the specified file for reading.
5761
*
@@ -74,7 +78,7 @@ public JournalReader(final DBBroker broker, final Path file, final int fileNumbe
7478

7579
private void validateJournalHeader(final Path file, final SeekableByteChannel fc) throws IOException, LogException {
7680
// read the magic number
77-
final ByteBuffer buf = ByteBuffer.allocate(JOURNAL_HEADER_LEN);
81+
final ByteBuffer buf = ByteBuffer.allocateDirect(JOURNAL_HEADER_LEN);
7882
fc.read(buf);
7983
buf.flip();
8084

@@ -137,18 +141,18 @@ Loggable previousEntry() throws LogException {
137141
return null;
138142
}
139143

140-
// go back two bytes and read the back-link of the last entry
141-
fc.position(fc.position() - LOG_ENTRY_BACK_LINK_LEN);
144+
// go back 8 bytes (checksum length) + 2 bytes (backLink length) and read the backLink (2 bytes) of the last entry
145+
fc.position(fc.position() - LOG_ENTRY_CHECKSUM_LEN - LOG_ENTRY_BACK_LINK_LEN);
142146
header.clear().limit(LOG_ENTRY_BACK_LINK_LEN);
143147
final int read = fc.read(header);
144148
if (read != LOG_ENTRY_BACK_LINK_LEN) {
145149
throw new LogException("Unable to read journal entry back-link!");
146150
}
147151
header.flip();
148-
final short prevLink = header.getShort();
152+
final short backLink = header.getShort();
149153

150154
// position the channel to the start of the previous entry and mark it
151-
final long prevStart = fc.position() - LOG_ENTRY_BACK_LINK_LEN - prevLink;
155+
final long prevStart = fc.position() - LOG_ENTRY_BACK_LINK_LEN - backLink;
152156
fc.position(prevStart);
153157
final Loggable loggable = readEntry();
154158

@@ -204,6 +208,19 @@ Loggable readEntry() throws LogException {
204208
}
205209
header.flip();
206210

211+
// prepare the checksum for the header
212+
xxHash64.reset();
213+
if (header.hasArray()) {
214+
xxHash64.update(header.array(), 0, LOG_ENTRY_HEADER_LEN);
215+
} else {
216+
final int mark = header.position();
217+
header.position(0);
218+
final byte buf[] = new byte[LOG_ENTRY_HEADER_LEN];
219+
header.get(buf);
220+
xxHash64.update(buf, 0, LOG_ENTRY_HEADER_LEN);
221+
header.position(mark);
222+
}
223+
207224
final byte entryType = header.get();
208225
final long transactId = header.getLong();
209226
final short size = header.getShort();
@@ -218,23 +235,51 @@ Loggable readEntry() throws LogException {
218235
}
219236
loggable.setLsn(lsn);
220237

221-
if (size + LOG_ENTRY_BACK_LINK_LEN > payload.capacity()) {
238+
final int remainingEntryBytes = size + LOG_ENTRY_BACK_LINK_LEN + LOG_ENTRY_CHECKSUM_LEN;
239+
240+
if (remainingEntryBytes > payload.capacity()) {
222241
// resize the payload buffer
223-
payload = ByteBuffer.allocate(size + LOG_ENTRY_BACK_LINK_LEN);
242+
payload = ByteBuffer.allocateDirect(remainingEntryBytes);
224243
}
225-
payload.clear().limit(size + LOG_ENTRY_BACK_LINK_LEN);
244+
payload.clear().limit(remainingEntryBytes);
226245
read = fc.read(payload);
227-
if (read < size + LOG_ENTRY_BACK_LINK_LEN) {
246+
if (read < remainingEntryBytes) {
228247
throw new LogException("Incomplete log entry found!");
229248
}
230249
payload.flip();
250+
251+
// read entry data
231252
loggable.read(payload);
232-
final short prevLink = payload.getShort();
233-
if (prevLink != size + LOG_ENTRY_HEADER_LEN) {
234-
LOG.error("Bad pointer to previous: prevLink = " + prevLink + "; size = " + size +
253+
254+
// read entry backLink
255+
final short backLink = payload.getShort();
256+
if (backLink != size + LOG_ENTRY_HEADER_LEN) {
257+
LOG.error("Bad pointer to previous: backLink = " + backLink + "; size = " + size +
235258
"; transactId = " + transactId);
236259
throw new LogException("Bad pointer to previous in entry: " + loggable.dump());
237260
}
261+
262+
// update the checksum for the entry data and backLink
263+
if (payload.hasArray()) {
264+
xxHash64.update(payload.array(), 0, size + LOG_ENTRY_BACK_LINK_LEN);
265+
} else {
266+
final int mark = payload.position();
267+
payload.position(0);
268+
final byte buf[] = new byte[size + LOG_ENTRY_BACK_LINK_LEN];
269+
payload.get(buf);
270+
xxHash64.update(buf, 0, size + LOG_ENTRY_BACK_LINK_LEN);
271+
payload.position(mark);
272+
}
273+
274+
// read the entry checksum
275+
final long checksum = payload.getLong();
276+
277+
// verify the checksum
278+
final long calculatedChecksum = xxHash64.getValue();
279+
if (checksum != calculatedChecksum) {
280+
throw new LogException("Checksum mismatch whilst reading log entry. read=" + checksum + " calculated=" + calculatedChecksum);
281+
}
282+
238283
return loggable;
239284
} catch (final IOException e) {
240285
throw new LogException(e.getMessage(), e);

0 commit comments

Comments
 (0)