diff --git a/bin/zkTxnLogToolkit.cmd b/bin/zkTxnLogToolkit.cmd
new file mode 100755
index 00000000000..362dc44b027
--- /dev/null
+++ b/bin/zkTxnLogToolkit.cmd
@@ -0,0 +1,24 @@
+@echo off
+REM Licensed to the Apache Software Foundation (ASF) under one or more
+REM contributor license agreements. See the NOTICE file distributed with
+REM this work for additional information regarding copyright ownership.
+REM The ASF licenses this file to You under the Apache License, Version 2.0
+REM (the "License"); you may not use this file except in compliance with
+REM the License. You may obtain a copy of the License at
+REM
+REM http://www.apache.org/licenses/LICENSE-2.0
+REM
+REM Unless required by applicable law or agreed to in writing, software
+REM distributed under the License is distributed on an "AS IS" BASIS,
+REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+REM See the License for the specific language governing permissions and
+REM limitations under the License.
+
+setlocal
+call "%~dp0zkEnv.cmd"
+
+set ZOOMAIN=org.apache.zookeeper.server.persistence.TxnLogToolkit
+call %JAVA% -cp "%CLASSPATH%" %ZOOMAIN% %*
+
+endlocal
+
diff --git a/bin/zkTxnLogToolkit.sh b/bin/zkTxnLogToolkit.sh
new file mode 100755
index 00000000000..8beed20ddd7
--- /dev/null
+++ b/bin/zkTxnLogToolkit.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+# 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.
+
+#
+# If this scripted is run out of /usr/bin or some other system bin directory
+# it should be linked to and not copied. Things like java jar files are found
+# relative to the canonical path of this script.
+#
+
+# use POSIX interface, symlink is followed automatically
+ZOOBIN="${BASH_SOURCE-$0}"
+ZOOBIN="$(dirname "${ZOOBIN}")"
+ZOOBINDIR="$(cd "${ZOOBIN}"; pwd)"
+
+if [ -e "$ZOOBIN/../libexec/zkEnv.sh" ]; then
+ . "$ZOOBINDIR"/../libexec/zkEnv.sh
+else
+ . "$ZOOBINDIR"/zkEnv.sh
+fi
+
+"$JAVA" -cp "$CLASSPATH" $JVMFLAGS \
+ org.apache.zookeeper.server.persistence.TxnLogToolkit "$@"
+
+
diff --git a/docs/bookkeeperConfig.pdf b/docs/bookkeeperConfig.pdf
index c62f4933a0d..07b8b9c573b 100644
Binary files a/docs/bookkeeperConfig.pdf and b/docs/bookkeeperConfig.pdf differ
diff --git a/docs/bookkeeperOverview.pdf b/docs/bookkeeperOverview.pdf
index 25880ad63a1..58c3b81dd32 100644
Binary files a/docs/bookkeeperOverview.pdf and b/docs/bookkeeperOverview.pdf differ
diff --git a/docs/bookkeeperProgrammer.pdf b/docs/bookkeeperProgrammer.pdf
index dbf3cb61a5c..efe127b46d6 100644
Binary files a/docs/bookkeeperProgrammer.pdf and b/docs/bookkeeperProgrammer.pdf differ
diff --git a/docs/bookkeeperStarted.pdf b/docs/bookkeeperStarted.pdf
index 483d9f74a8c..bca40a5efc2 100644
Binary files a/docs/bookkeeperStarted.pdf and b/docs/bookkeeperStarted.pdf differ
diff --git a/docs/bookkeeperStream.pdf b/docs/bookkeeperStream.pdf
index 5b5ca786d87..7d602ddbfaf 100644
Binary files a/docs/bookkeeperStream.pdf and b/docs/bookkeeperStream.pdf differ
diff --git a/docs/index.pdf b/docs/index.pdf
index bd53a29c28d..de75668548e 100644
Binary files a/docs/index.pdf and b/docs/index.pdf differ
diff --git a/docs/javaExample.pdf b/docs/javaExample.pdf
index f34daa4e8c7..a5a2e745d83 100644
Binary files a/docs/javaExample.pdf and b/docs/javaExample.pdf differ
diff --git a/docs/linkmap.pdf b/docs/linkmap.pdf
index 0f8bcad2efe..d756b22e095 100644
Binary files a/docs/linkmap.pdf and b/docs/linkmap.pdf differ
diff --git a/docs/recipes.pdf b/docs/recipes.pdf
index 5aab9ad0e84..3c4cb9e8476 100644
Binary files a/docs/recipes.pdf and b/docs/recipes.pdf differ
diff --git a/docs/zookeeperAdmin.html b/docs/zookeeperAdmin.html
index f8166f9f8b7..9f00dba81de 100644
--- a/docs/zookeeperAdmin.html
+++ b/docs/zookeeperAdmin.html
@@ -316,6 +316,9 @@
@@ -2063,6 +2066,65 @@ File Management
+
+Recovery - TxnLogToolkit
+TxnLogToolkit is a command line tool shipped with ZooKeeper which
+ is capable of recovering transaction log entries with broken CRC.
+Running it without any command line parameters or with the "-h,--help"
+ argument, it outputs the following help page:
+
+ $ bin/zkTxnLogToolkit.sh
+
+ usage: TxnLogToolkit [-dhrv] txn_log_file_name
+ -d,--dump Dump mode. Dump all entries of the log file. (this is the default)
+ -h,--help Print help message
+ -r,--recover Recovery mode. Re-calculate CRC for broken entries.
+ -v,--verbose Be verbose in recovery mode: print all entries, not just fixed ones.
+ -y,--yes Non-interactive mode: repair all CRC errors without asking
+
+The default behaviour is safe: it dumps the entries of the given
+ transaction log file to the screen: (same as using '-d,--dump' parameter)
+
+ $ bin/zkTxnLogToolkit.sh log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ 4/5/18 2:15:58 PM CEST session 0x16295bafcc40000 cxid 0x0 zxid 0x100000001 createSession 30000
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ 4/5/18 2:16:12 PM CEST session 0x26295bafcc90000 cxid 0x0 zxid 0x100000003 createSession 30000
+ 4/5/18 2:17:34 PM CEST session 0x26295bafcc90000 cxid 0x0 zxid 0x200000001 closeSession null
+ 4/5/18 2:17:34 PM CEST session 0x16295bd23720000 cxid 0x0 zxid 0x200000002 createSession 30000
+ 4/5/18 2:18:02 PM CEST session 0x16295bd23720000 cxid 0x2 zxid 0x200000003 create '/andor,#626262,v{s{31,s{'world,'anyone}}},F,1
+ EOF reached after 6 txns.
+
+There's a CRC error in the 2nd entry of the above transaction log file. In dump
+ mode, the toolkit only prints this information to the screen without touching the original file. In
+ recovery mode (-r,--recover flag) the original file still remains
+ untouched and all transactions will be copied over to a new txn log file with ".fixed" suffix. It recalculates
+ CRC values and copies the calculated value, if it doesn't match the original txn entry.
+ By default, the tool works interactively: it asks for confirmation whenever CRC error encountered.
+
+ $ bin/zkTxnLogToolkit.sh -r log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ Would you like to fix it (Yes/No/Abort) ?
+
+Answering Yes means the newly calculated CRC value will be outputted
+ to the new file. No means that the original CRC value will be copied over.
+ Abort will abort the entire operation and exits.
+ (In this case the ".fixed" will not be deleted and left in a half-complete state: contains only entries which
+ have already been processed or only the header if the operation was aborted at the first entry.)
+
+ $ bin/zkTxnLogToolkit.sh -r log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ Would you like to fix it (Yes/No/Abort) ? y
+ EOF reached after 6 txns.
+ Recovery file log.100000001.fixed has been written with 1 fixed CRC error(s)
+
+The default behaviour of recovery is to be silent: only entries with CRC error get printed to the screen.
+ One can turn on verbose mode with the -v,--verbose parameter to see all records.
+ Interactive mode can be turned off with the -y,--yes parameter. In this case all CRC errors will be fixed
+ in the new transaction file.
Things to Avoid
Here are some common problems you can avoid by configuring
diff --git a/docs/zookeeperAdmin.pdf b/docs/zookeeperAdmin.pdf
index 0004649cd56..21728be6547 100644
Binary files a/docs/zookeeperAdmin.pdf and b/docs/zookeeperAdmin.pdf differ
diff --git a/docs/zookeeperHierarchicalQuorums.pdf b/docs/zookeeperHierarchicalQuorums.pdf
index f86909f1a83..d0db17fdef9 100644
Binary files a/docs/zookeeperHierarchicalQuorums.pdf and b/docs/zookeeperHierarchicalQuorums.pdf differ
diff --git a/docs/zookeeperInternals.pdf b/docs/zookeeperInternals.pdf
index 8bcd192f783..7ca1e0bba77 100644
Binary files a/docs/zookeeperInternals.pdf and b/docs/zookeeperInternals.pdf differ
diff --git a/docs/zookeeperJMX.pdf b/docs/zookeeperJMX.pdf
index 8b74b239eb7..56d67e10daa 100644
Binary files a/docs/zookeeperJMX.pdf and b/docs/zookeeperJMX.pdf differ
diff --git a/docs/zookeeperObservers.pdf b/docs/zookeeperObservers.pdf
index 29ebbb029d2..f793f34b248 100644
Binary files a/docs/zookeeperObservers.pdf and b/docs/zookeeperObservers.pdf differ
diff --git a/docs/zookeeperOver.pdf b/docs/zookeeperOver.pdf
index 8839eef6474..c26219781d9 100644
Binary files a/docs/zookeeperOver.pdf and b/docs/zookeeperOver.pdf differ
diff --git a/docs/zookeeperProgrammers.pdf b/docs/zookeeperProgrammers.pdf
index 8448d313040..b375fafd4ee 100644
Binary files a/docs/zookeeperProgrammers.pdf and b/docs/zookeeperProgrammers.pdf differ
diff --git a/docs/zookeeperQuotas.pdf b/docs/zookeeperQuotas.pdf
index 6a95063ed0d..d6894c61c41 100644
Binary files a/docs/zookeeperQuotas.pdf and b/docs/zookeeperQuotas.pdf differ
diff --git a/docs/zookeeperStarted.pdf b/docs/zookeeperStarted.pdf
index e6e15ccc4b4..cb52a80b6ba 100644
Binary files a/docs/zookeeperStarted.pdf and b/docs/zookeeperStarted.pdf differ
diff --git a/docs/zookeeperTutorial.pdf b/docs/zookeeperTutorial.pdf
index 70df2639583..543177c8545 100644
Binary files a/docs/zookeeperTutorial.pdf and b/docs/zookeeperTutorial.pdf differ
diff --git a/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml b/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml
index 447b90c364d..2f3a57fac86 100644
--- a/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml
+++ b/src/docs/src/documentation/content/xdocs/zookeeperAdmin.xml
@@ -1702,6 +1702,76 @@ imok
individual settings in which it is being deployed.
+
+
+ Recovery - TxnLogToolkit
+
+ TxnLogToolkit is a command line tool shipped with ZooKeeper which
+ is capable of recovering transaction log entries with broken CRC.
+ Running it without any command line parameters or with the "-h,--help"
+ argument, it outputs the following help page:
+
+
+ $ bin/zkTxnLogToolkit.sh
+
+ usage: TxnLogToolkit [-dhrv] txn_log_file_name
+ -d,--dump Dump mode. Dump all entries of the log file. (this is the default)
+ -h,--help Print help message
+ -r,--recover Recovery mode. Re-calculate CRC for broken entries.
+ -v,--verbose Be verbose in recovery mode: print all entries, not just fixed ones.
+ -y,--yes Non-interactive mode: repair all CRC errors without asking
+
+
+ The default behaviour is safe: it dumps the entries of the given
+ transaction log file to the screen: (same as using '-d,--dump' parameter)
+
+
+ $ bin/zkTxnLogToolkit.sh log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ 4/5/18 2:15:58 PM CEST session 0x16295bafcc40000 cxid 0x0 zxid 0x100000001 createSession 30000
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ 4/5/18 2:16:12 PM CEST session 0x26295bafcc90000 cxid 0x0 zxid 0x100000003 createSession 30000
+ 4/5/18 2:17:34 PM CEST session 0x26295bafcc90000 cxid 0x0 zxid 0x200000001 closeSession null
+ 4/5/18 2:17:34 PM CEST session 0x16295bd23720000 cxid 0x0 zxid 0x200000002 createSession 30000
+ 4/5/18 2:18:02 PM CEST session 0x16295bd23720000 cxid 0x2 zxid 0x200000003 create '/andor,#626262,v{s{31,s{'world,'anyone}}},F,1
+ EOF reached after 6 txns.
+
+
+ There's a CRC error in the 2nd entry of the above transaction log file. In dump
+ mode, the toolkit only prints this information to the screen without touching the original file. In
+ recovery mode (-r,--recover flag) the original file still remains
+ untouched and all transactions will be copied over to a new txn log file with ".fixed" suffix. It recalculates
+ CRC values and copies the calculated value, if it doesn't match the original txn entry.
+ By default, the tool works interactively: it asks for confirmation whenever CRC error encountered.
+
+
+ $ bin/zkTxnLogToolkit.sh -r log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ Would you like to fix it (Yes/No/Abort) ?
+
+
+ Answering Yes means the newly calculated CRC value will be outputted
+ to the new file. No means that the original CRC value will be copied over.
+ Abort will abort the entire operation and exits.
+ (In this case the ".fixed" will not be deleted and left in a half-complete state: contains only entries which
+ have already been processed or only the header if the operation was aborted at the first entry.)
+
+
+ $ bin/zkTxnLogToolkit.sh -r log.100000001
+ ZooKeeper Transactional Log File with dbid 0 txnlog format version 2
+ CRC ERROR - 4/5/18 2:16:05 PM CEST session 0x16295bafcc40000 cxid 0x1 zxid 0x100000002 closeSession null
+ Would you like to fix it (Yes/No/Abort) ? y
+ EOF reached after 6 txns.
+ Recovery file log.100000001.fixed has been written with 1 fixed CRC error(s)
+
+
+ The default behaviour of recovery is to be silent: only entries with CRC error get printed to the screen.
+ One can turn on verbose mode with the -v,--verbose parameter to see all records.
+ Interactive mode can be turned off with the -y,--yes parameter. In this case all CRC errors will be fixed
+ in the new transaction file.
+
diff --git a/src/java/main/org/apache/zookeeper/server/TraceFormatter.java b/src/java/main/org/apache/zookeeper/server/TraceFormatter.java
index 60d1cc7580b..66cc31ca38b 100644
--- a/src/java/main/org/apache/zookeeper/server/TraceFormatter.java
+++ b/src/java/main/org/apache/zookeeper/server/TraceFormatter.java
@@ -29,7 +29,7 @@
public class TraceFormatter {
- static String op2String(int op) {
+ public static String op2String(int op) {
switch (op) {
case OpCode.notification:
return "notification";
diff --git a/src/java/main/org/apache/zookeeper/server/persistence/FilePadding.java b/src/java/main/org/apache/zookeeper/server/persistence/FilePadding.java
new file mode 100644
index 00000000000..c4052e95843
--- /dev/null
+++ b/src/java/main/org/apache/zookeeper/server/persistence/FilePadding.java
@@ -0,0 +1,105 @@
+/**
+ * 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.zookeeper.server.persistence;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+public class FilePadding {
+ private static final Logger LOG;
+ private static long preAllocSize = 65536 * 1024;
+ private static final ByteBuffer fill = ByteBuffer.allocateDirect(1);
+
+ static {
+ LOG = LoggerFactory.getLogger(FileTxnLog.class);
+
+ String size = System.getProperty("zookeeper.preAllocSize");
+ if (size != null) {
+ try {
+ preAllocSize = Long.parseLong(size) * 1024;
+ } catch (NumberFormatException e) {
+ LOG.warn(size + " is not a valid value for preAllocSize");
+ }
+ }
+ }
+
+ private long currentSize;
+
+ /**
+ * method to allow setting preallocate size
+ * of log file to pad the file.
+ *
+ * @param size the size to set to in bytes
+ */
+ public static void setPreallocSize(long size) {
+ preAllocSize = size;
+ }
+
+ public void setCurrentSize(long currentSize) {
+ this.currentSize = currentSize;
+ }
+
+ /**
+ * pad the current file to increase its size to the next multiple of preAllocSize greater than the current size and position
+ *
+ * @param fileChannel the fileChannel of the file to be padded
+ * @throws IOException
+ */
+ long padFile(FileChannel fileChannel) throws IOException {
+ long newFileSize = calculateFileSizeWithPadding(fileChannel.position(), currentSize, preAllocSize);
+ if (currentSize != newFileSize) {
+ fileChannel.write((ByteBuffer) fill.position(0), newFileSize - fill.remaining());
+ currentSize = newFileSize;
+ }
+ return currentSize;
+ }
+
+ /**
+ * Calculates a new file size with padding. We only return a new size if
+ * the current file position is sufficiently close (less than 4K) to end of
+ * file and preAllocSize is > 0.
+ *
+ * @param position the point in the file we have written to
+ * @param fileSize application keeps track of the current file size
+ * @param preAllocSize how many bytes to pad
+ * @return the new file size. It can be the same as fileSize if no
+ * padding was done.
+ * @throws IOException
+ */
+ // VisibleForTesting
+ public static long calculateFileSizeWithPadding(long position, long fileSize, long preAllocSize) {
+ // If preAllocSize is positive and we are within 4KB of the known end of the file calculate a new file size
+ if (preAllocSize > 0 && position + 4096 >= fileSize) {
+ // If we have written more than we have previously preallocated we need to make sure the new
+ // file size is larger than what we already have
+ if (position > fileSize) {
+ fileSize = position + preAllocSize;
+ fileSize -= fileSize % preAllocSize;
+ } else {
+ fileSize += preAllocSize;
+ }
+ }
+
+ return fileSize;
+ }
+}
diff --git a/src/java/main/org/apache/zookeeper/server/persistence/FileTxnLog.java b/src/java/main/org/apache/zookeeper/server/persistence/FileTxnLog.java
index 3694c985ec3..15c7211fa8d 100644
--- a/src/java/main/org/apache/zookeeper/server/persistence/FileTxnLog.java
+++ b/src/java/main/org/apache/zookeeper/server/persistence/FileTxnLog.java
@@ -91,9 +91,6 @@
public class FileTxnLog implements TxnLog {
private static final Logger LOG;
- static long preAllocSize = 65536 * 1024;
- private static final ByteBuffer fill = ByteBuffer.allocateDirect(1);
-
public final static int TXNLOG_MAGIC =
ByteBuffer.wrap("ZKLG".getBytes()).getInt();
@@ -107,14 +104,6 @@ public class FileTxnLog implements TxnLog {
static {
LOG = LoggerFactory.getLogger(FileTxnLog.class);
- String size = System.getProperty("zookeeper.preAllocSize");
- if (size != null) {
- try {
- preAllocSize = Long.parseLong(size) * 1024;
- } catch (NumberFormatException e) {
- LOG.warn(size + " is not a valid value for preAllocSize");
- }
- }
/** Local variable to read fsync.warningthresholdms into */
Long fsyncWarningThreshold;
if ((fsyncWarningThreshold = Long.getLong("zookeeper.fsync.warningthresholdms")) == null)
@@ -132,8 +121,8 @@ public class FileTxnLog implements TxnLog {
long dbId;
private LinkedList streamsToFlush =
new LinkedList();
- long currentSize;
File logFileWrite = null;
+ private FilePadding filePadding = new FilePadding();
/**
* constructor for FileTxnLog. Take the directory
@@ -144,15 +133,6 @@ public FileTxnLog(File logDir) {
this.logDir = logDir;
}
- /**
- * method to allow setting preallocate size
- * of log file to pad the file.
- * @param size the size to set to in bytes
- */
- public static void setPreallocSize(long size) {
- preAllocSize = size;
- }
-
/**
* creates a checksum alogrithm to be used
* @return the checksum used for this txnlog
@@ -161,7 +141,6 @@ protected Checksum makeChecksumAlgorithm(){
return new Adler32();
}
-
/**
* rollover the current log file to a new one.
* @throws IOException
@@ -213,18 +192,18 @@ public synchronized boolean append(TxnHeader hdr, Record txn)
LOG.info("Creating new log file: " + Util.makeLogName(hdr.getZxid()));
}
- logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid()));
- fos = new FileOutputStream(logFileWrite);
- logStream=new BufferedOutputStream(fos);
- oa = BinaryOutputArchive.getArchive(logStream);
- FileHeader fhdr = new FileHeader(TXNLOG_MAGIC,VERSION, dbId);
- fhdr.serialize(oa, "fileheader");
- // Make sure that the magic number is written before padding.
- logStream.flush();
- currentSize = fos.getChannel().position();
- streamsToFlush.add(fos);
- }
- currentSize = padFile(fos.getChannel());
+ logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid()));
+ fos = new FileOutputStream(logFileWrite);
+ logStream=new BufferedOutputStream(fos);
+ oa = BinaryOutputArchive.getArchive(logStream);
+ FileHeader fhdr = new FileHeader(TXNLOG_MAGIC,VERSION, dbId);
+ fhdr.serialize(oa, "fileheader");
+ // Make sure that the magic number is written before padding.
+ logStream.flush();
+ filePadding.setCurrentSize(fos.getChannel().position());
+ streamsToFlush.add(fos);
+ }
+ filePadding.padFile(fos.getChannel());
byte[] buf = Util.marshallTxnEntry(hdr, txn);
if (buf == null || buf.length == 0) {
throw new IOException("Faulty serialization for header " +
@@ -238,49 +217,6 @@ public synchronized boolean append(TxnHeader hdr, Record txn)
return true;
}
- /**
- * pad the current file to increase its size to the next multiple of preAllocSize greater than the current size and position
- * @param fileChannel the fileChannel of the file to be padded
- * @throws IOException
- */
- private long padFile(FileChannel fileChannel) throws IOException {
- long newFileSize = calculateFileSizeWithPadding(fileChannel.position(), currentSize, preAllocSize);
- if (currentSize != newFileSize) {
- fileChannel.write((ByteBuffer) fill.position(0), newFileSize - fill.remaining());
- currentSize = newFileSize;
- }
- return currentSize;
- }
-
- /**
- * Calculates a new file size with padding. We only return a new size if
- * the current file position is sufficiently close (less than 4K) to end of
- * file and preAllocSize is > 0.
- *
- * @param position the point in the file we have written to
- * @param fileSize application keeps track of the current file size
- * @param preAllocSize how many bytes to pad
- * @return the new file size. It can be the same as fileSize if no
- * padding was done.
- * @throws IOException
- */
- // VisibleForTesting
- public static long calculateFileSizeWithPadding(long position, long fileSize, long preAllocSize) {
- // If preAllocSize is positive and we are within 4KB of the known end of the file calculate a new file size
- if (preAllocSize > 0 && position + 4096 >= fileSize) {
- // If we have written more than we have previously preallocated we need to make sure the new
- // file size is larger than what we already have
- if (position > fileSize){
- fileSize = position + preAllocSize;
- fileSize -= fileSize % preAllocSize;
- } else {
- fileSize += preAllocSize;
- }
- }
-
- return fileSize;
- }
-
/**
* Find the log file that starts at, or just before, the snapshot. Return
* this and all subsequent logs. Results are ordered by zxid of file,
diff --git a/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkit.java b/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkit.java
new file mode 100644
index 00000000000..432cc8809c4
--- /dev/null
+++ b/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkit.java
@@ -0,0 +1,280 @@
+/**
+ * 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.zookeeper.server.persistence;
+
+import org.apache.jute.BinaryInputArchive;
+import org.apache.jute.BinaryOutputArchive;
+import org.apache.jute.Record;
+import org.apache.zookeeper.server.TraceFormatter;
+import org.apache.zookeeper.server.util.SerializeUtils;
+import org.apache.zookeeper.txn.TxnHeader;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Scanner;
+import java.util.zip.Adler32;
+import java.util.zip.Checksum;
+
+import static org.apache.zookeeper.server.persistence.FileTxnLog.TXNLOG_MAGIC;
+import static org.apache.zookeeper.server.persistence.TxnLogToolkitCliParser.printHelpAndExit;
+
+public class TxnLogToolkit implements Closeable {
+
+ static class TxnLogToolkitException extends Exception {
+ private static final long serialVersionUID = 1L;
+ private int exitCode;
+
+ TxnLogToolkitException(int exitCode, String message, Object... params) {
+ super(String.format(message, params));
+ this.exitCode = exitCode;
+ }
+
+ int getExitCode() {
+ return exitCode;
+ }
+ }
+
+ static class TxnLogToolkitParseException extends TxnLogToolkitException {
+ private static final long serialVersionUID = 1L;
+
+ TxnLogToolkitParseException(int exitCode, String message, Object... params) {
+ super(exitCode, message, params);
+ }
+ }
+
+ private File txnLogFile;
+ private boolean recoveryMode = false;
+ private boolean verbose = false;
+ private FileInputStream txnFis;
+ private BinaryInputArchive logStream;
+
+ // Recovery mode
+ private int crcFixed = 0;
+ private FileOutputStream recoveryFos;
+ private BinaryOutputArchive recoveryOa;
+ private File recoveryLogFile;
+ private FilePadding filePadding = new FilePadding();
+ private boolean force = false;
+
+ /**
+ * @param args Command line arguments
+ */
+ public static void main(String[] args) throws Exception {
+ final TxnLogToolkit lt = parseCommandLine(args);
+ try {
+ lt.dump(new InputStreamReader(System.in));
+ lt.printStat();
+ } catch (TxnLogToolkitParseException e) {
+ System.err.println(e.getMessage() + "\n");
+ printHelpAndExit(e.getExitCode());
+ } catch (TxnLogToolkitException e) {
+ System.err.println(e.getMessage());
+ System.exit(e.getExitCode());
+ } finally {
+ lt.close();
+ }
+ }
+
+ public TxnLogToolkit(boolean recoveryMode, boolean verbose, String txnLogFileName, boolean force)
+ throws FileNotFoundException, TxnLogToolkitException {
+ this.recoveryMode = recoveryMode;
+ this.verbose = verbose;
+ this.force = force;
+ txnLogFile = new File(txnLogFileName);
+ if (!txnLogFile.exists() || !txnLogFile.canRead()) {
+ throw new TxnLogToolkitException(1, "File doesn't exist or not readable: %s", txnLogFile);
+ }
+ if (recoveryMode) {
+ recoveryLogFile = new File(txnLogFile.toString() + ".fixed");
+ if (recoveryLogFile.exists()) {
+ throw new TxnLogToolkitException(1, "Recovery file %s already exists or not writable", recoveryLogFile);
+ }
+ }
+
+ openTxnLogFile();
+ if (recoveryMode) {
+ openRecoveryFile();
+ }
+ }
+
+ public void dump(Reader input) throws Exception {
+ crcFixed = 0;
+
+ FileHeader fhdr = new FileHeader();
+ fhdr.deserialize(logStream, "fileheader");
+ if (fhdr.getMagic() != TXNLOG_MAGIC) {
+ throw new TxnLogToolkitException(2, "Invalid magic number for %s", txnLogFile.getName());
+ }
+ System.out.println("ZooKeeper Transactional Log File with dbid "
+ + fhdr.getDbid() + " txnlog format version "
+ + fhdr.getVersion());
+
+ if (recoveryMode) {
+ fhdr.serialize(recoveryOa, "fileheader");
+ recoveryFos.flush();
+ filePadding.setCurrentSize(recoveryFos.getChannel().position());
+ }
+
+ int count = 0;
+ while (true) {
+ long crcValue;
+ byte[] bytes;
+ try {
+ crcValue = logStream.readLong("crcvalue");
+ bytes = logStream.readBuffer("txnEntry");
+ } catch (EOFException e) {
+ System.out.println("EOF reached after " + count + " txns.");
+ return;
+ }
+ if (bytes.length == 0) {
+ // Since we preallocate, we define EOF to be an
+ // empty transaction
+ System.out.println("EOF reached after " + count + " txns.");
+ return;
+ }
+ Checksum crc = new Adler32();
+ crc.update(bytes, 0, bytes.length);
+ if (crcValue != crc.getValue()) {
+ if (recoveryMode) {
+ if (!force) {
+ printTxn(bytes, "CRC ERROR");
+ if (askForFix(input)) {
+ crcValue = crc.getValue();
+ ++crcFixed;
+ }
+ } else {
+ crcValue = crc.getValue();
+ printTxn(bytes, "CRC FIXED");
+ ++crcFixed;
+ }
+ } else {
+ printTxn(bytes, "CRC ERROR");
+ }
+ }
+ if (!recoveryMode || verbose) {
+ printTxn(bytes);
+ }
+ if (logStream.readByte("EOR") != 'B') {
+ throw new TxnLogToolkitException(1, "Last transaction was partial.");
+ }
+ if (recoveryMode) {
+ filePadding.padFile(recoveryFos.getChannel());
+ recoveryOa.writeLong(crcValue, "crcvalue");
+ recoveryOa.writeBuffer(bytes, "txnEntry");
+ recoveryOa.writeByte((byte)'B', "EOR");
+ }
+ count++;
+ }
+ }
+
+ private boolean askForFix(Reader input) throws TxnLogToolkitException {
+ Scanner scanner = new Scanner(input);
+ try {
+ while (true) {
+ System.out.print("Would you like to fix it (Yes/No/Abort) ? ");
+ char answer = Character.toUpperCase(scanner.next().charAt(0));
+ switch (answer) {
+ case 'Y':
+ return true;
+ case 'N':
+ return false;
+ case 'A':
+ throw new TxnLogToolkitException(0, "Recovery aborted.");
+ }
+ }
+ } finally {
+ scanner.close();
+ }
+ }
+
+ private void printTxn(byte[] bytes) throws IOException {
+ printTxn(bytes, "");
+ }
+
+ private void printTxn(byte[] bytes, String prefix) throws IOException {
+ TxnHeader hdr = new TxnHeader();
+ Record txn = SerializeUtils.deserializeTxn(bytes, hdr);
+ String txns = String.format("%s session 0x%s cxid 0x%s zxid 0x%s %s %s",
+ DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG).format(new Date(hdr.getTime())),
+ Long.toHexString(hdr.getClientId()),
+ Long.toHexString(hdr.getCxid()),
+ Long.toHexString(hdr.getZxid()),
+ TraceFormatter.op2String(hdr.getType()),
+ txn);
+ if (prefix != null && !"".equals(prefix.trim())) {
+ System.out.print(prefix + " - ");
+ }
+ if (txns.endsWith("\n")) {
+ System.out.print(txns);
+ } else {
+ System.out.println(txns);
+ }
+ }
+
+ private void openTxnLogFile() throws FileNotFoundException {
+ txnFis = new FileInputStream(txnLogFile);
+ logStream = BinaryInputArchive.getArchive(txnFis);
+ }
+
+ private void closeTxnLogFile() throws IOException {
+ if (txnFis != null) {
+ txnFis.close();
+ }
+ }
+
+ private void openRecoveryFile() throws FileNotFoundException {
+ recoveryFos = new FileOutputStream(recoveryLogFile);
+ recoveryOa = BinaryOutputArchive.getArchive(recoveryFos);
+ }
+
+ private void closeRecoveryFile() throws IOException {
+ if (recoveryFos != null) {
+ recoveryFos.close();
+ }
+ }
+
+ private static TxnLogToolkit parseCommandLine(String[] args) throws TxnLogToolkitException, FileNotFoundException {
+ TxnLogToolkitCliParser parser = new TxnLogToolkitCliParser();
+ parser.parse(args);
+ return new TxnLogToolkit(parser.isRecoveryMode(), parser.isVerbose(), parser.getTxnLogFileName(), parser.isForce());
+ }
+
+ private void printStat() {
+ if (recoveryMode) {
+ System.out.printf("Recovery file %s has been written with %d fixed CRC error(s)%n", recoveryLogFile, crcFixed);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (recoveryMode) {
+ closeRecoveryFile();
+ }
+ closeTxnLogFile();
+ }
+}
diff --git a/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParser.java b/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParser.java
new file mode 100644
index 00000000000..094500a9f2d
--- /dev/null
+++ b/src/java/main/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParser.java
@@ -0,0 +1,101 @@
+/**
+ * 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.zookeeper.server.persistence;
+
+class TxnLogToolkitCliParser {
+ private String txnLogFileName;
+ private boolean recoveryMode;
+ private boolean verbose;
+ private boolean force;
+
+ String getTxnLogFileName() {
+ return txnLogFileName;
+ }
+
+ boolean isRecoveryMode() {
+ return recoveryMode;
+ }
+
+ boolean isVerbose() {
+ return verbose;
+ }
+
+ boolean isForce() {
+ return force;
+ }
+
+ void parse(String[] args) throws TxnLogToolkit.TxnLogToolkitParseException {
+ if (args == null) {
+ throw new TxnLogToolkit.TxnLogToolkitParseException(1, "No arguments given");
+ }
+ txnLogFileName = null;
+ for (String arg : args) {
+ if (arg.startsWith("--")) {
+ String par = arg.substring(2);
+ if ("help".equalsIgnoreCase(par)) {
+ printHelpAndExit(0);
+ } else if ("recover".equalsIgnoreCase(par)) {
+ recoveryMode = true;
+ } else if ("verbose".equalsIgnoreCase(par)) {
+ verbose = true;
+ } else if ("dump".equalsIgnoreCase(par)) {
+ recoveryMode = false;
+ } else if ("yes".equalsIgnoreCase(par)) {
+ force = true;
+ } else {
+ throw new TxnLogToolkit.TxnLogToolkitParseException(1, "Invalid argument: %s", par);
+ }
+ } else if (arg.startsWith("-")) {
+ String par = arg.substring(1);
+ if ("h".equalsIgnoreCase(par)) {
+ printHelpAndExit(0);
+ } else if ("r".equalsIgnoreCase(par)) {
+ recoveryMode = true;
+ } else if ("v".equalsIgnoreCase(par)) {
+ verbose = true;
+ } else if ("d".equalsIgnoreCase(par)) {
+ recoveryMode = false;
+ } else if ("y".equalsIgnoreCase(par)) {
+ force = true;
+ } else {
+ throw new TxnLogToolkit.TxnLogToolkitParseException(1, "Invalid argument: %s", par);
+ }
+ } else {
+ if (txnLogFileName != null) {
+ throw new TxnLogToolkit.TxnLogToolkitParseException(1, "Invalid arguments: more than one TXN log file given");
+ }
+ txnLogFileName = arg;
+ }
+ }
+
+ if (txnLogFileName == null) {
+ throw new TxnLogToolkit.TxnLogToolkitParseException(1, "Invalid arguments: TXN log file name missing");
+ }
+ }
+
+ static void printHelpAndExit(int exitCode) {
+ System.out.println("usage: TxnLogToolkit [-dhrvy] txn_log_file_name\n");
+ System.out.println(" -d,--dump Dump mode. Dump all entries of the log file. (this is the default)");
+ System.out.println(" -h,--help Print help message");
+ System.out.println(" -r,--recover Recovery mode. Re-calculate CRC for broken entries.");
+ System.out.println(" -v,--verbose Be verbose in recovery mode: print all entries, not just fixed ones.");
+ System.out.println(" -y,--yes Non-interactive mode: repair all CRC errors without asking");
+ System.exit(exitCode);
+ }
+}
diff --git a/src/java/test/data/invalidsnap/version-2/log.42 b/src/java/test/data/invalidsnap/version-2/log.42
new file mode 100644
index 00000000000..5385be516e2
Binary files /dev/null and b/src/java/test/data/invalidsnap/version-2/log.42 differ
diff --git a/src/java/test/org/apache/zookeeper/server/persistence/FileTxnLogTest.java b/src/java/test/org/apache/zookeeper/server/persistence/FileTxnLogTest.java
index 5f54d0e388f..97cbc372af4 100644
--- a/src/java/test/org/apache/zookeeper/server/persistence/FileTxnLogTest.java
+++ b/src/java/test/org/apache/zookeeper/server/persistence/FileTxnLogTest.java
@@ -39,27 +39,27 @@ public class FileTxnLogTest extends ZKTestCase {
@Test
public void testInvalidPreallocSize() {
Assert.assertEquals("file should not be padded",
- 10 * KB, FileTxnLog.calculateFileSizeWithPadding(7 * KB, 10 * KB, 0));
+ 10 * KB, FilePadding.calculateFileSizeWithPadding(7 * KB, 10 * KB, 0));
Assert.assertEquals("file should not be padded",
- 10 * KB, FileTxnLog.calculateFileSizeWithPadding(7 * KB, 10 * KB, -1));
+ 10 * KB, FilePadding.calculateFileSizeWithPadding(7 * KB, 10 * KB, -1));
}
@Test
public void testCalculateFileSizeWithPaddingWhenNotToCurrentSize() {
Assert.assertEquals("file should not be padded",
- 10 * KB, FileTxnLog.calculateFileSizeWithPadding(5 * KB, 10 * KB, 10 * KB));
+ 10 * KB, FilePadding.calculateFileSizeWithPadding(5 * KB, 10 * KB, 10 * KB));
}
@Test
public void testCalculateFileSizeWithPaddingWhenCloseToCurrentSize() {
Assert.assertEquals("file should be padded an additional 10 KB",
- 20 * KB, FileTxnLog.calculateFileSizeWithPadding(7 * KB, 10 * KB, 10 * KB));
+ 20 * KB, FilePadding.calculateFileSizeWithPadding(7 * KB, 10 * KB, 10 * KB));
}
@Test
public void testFileSizeGreaterThanPosition() {
Assert.assertEquals("file should be padded to 40 KB",
- 40 * KB, FileTxnLog.calculateFileSizeWithPadding(31 * KB, 10 * KB, 10 * KB));
+ 40 * KB, FilePadding.calculateFileSizeWithPadding(31 * KB, 10 * KB, 10 * KB));
}
@Test
@@ -69,7 +69,7 @@ public void testPreAllocSizeSmallerThanTxnData() throws IOException {
// Set a small preAllocSize (.5 MB)
final int preAllocSize = 500 * KB;
- fileTxnLog.setPreallocSize(preAllocSize);
+ FilePadding.setPreallocSize(preAllocSize);
// Create dummy txn larger than preAllocSize
// Since the file padding inserts a 0, we will fill the data with 0xff to ensure we corrupt the data if we put the 0 in the data
diff --git a/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParserTest.java b/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParserTest.java
new file mode 100644
index 00000000000..ee4dc061c71
--- /dev/null
+++ b/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitCliParserTest.java
@@ -0,0 +1,110 @@
+/**
+ * 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.zookeeper.server.persistence;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class TxnLogToolkitCliParserTest {
+
+ private TxnLogToolkitCliParser parser;
+
+ @Before
+ public void setUp() {
+ parser = new TxnLogToolkitCliParser();
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitParseException.class)
+ public void testParseWithNoArguments() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(null);
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitParseException.class)
+ public void testParseWithEmptyArgs() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[0]);
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitParseException.class)
+ public void testParseWith2Filenames() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "file1.log", "file2.log "});
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitParseException.class)
+ public void testParseWithInvalidShortSwitch() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-v", "-i", "txnlog.txt" });
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitParseException.class)
+ public void testParseWithInvalidLongSwitch() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-v", "--invalid", "txnlog.txt" });
+ }
+
+ @Test
+ public void testParseRecoveryModeSwitchShort() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-r", "txnlog.txt"});
+ assertThat("Recovery short switch should turn on recovery mode", parser.isRecoveryMode(), is(true));
+ }
+
+ @Test
+ public void testParseRecoveryModeSwitchLong() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "--recover", "txnlog.txt"});
+ assertThat("Recovery long switch should turn on recovery mode", parser.isRecoveryMode(), is(true));
+ }
+
+ @Test
+ public void testParseVerboseModeSwitchShort() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-v", "txnlog.txt"});
+ assertThat("Verbose short switch should turn on verbose mode", parser.isVerbose(), is(true));
+ }
+
+ @Test
+ public void testParseVerboseModeSwitchLong() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "--verbose", "txnlog.txt"});
+ assertThat("Verbose long switch should turn on verbose mode", parser.isVerbose(), is(true));
+ }
+
+ @Test
+ public void testParseDumpModeSwitchShort() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-r", "txnlog.txt"}); // turn on
+ parser.parse(new String[] { "-d", "txnlog.txt"}); // turn off
+ assertThat("Dump short switch should turn off recover mode", parser.isRecoveryMode(), is(false));
+ }
+
+ @Test
+ public void testParseDumpModeSwitchLong() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-r", "txnlog.txt"}); // turn on
+ parser.parse(new String[] { "--dump", "txnlog.txt"}); // turn off
+ assertThat("Dump long switch should turn off recovery mode", parser.isRecoveryMode(), is(false));
+ }
+
+ @Test
+ public void testParseForceModeSwitchShort() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "-y", "txnlog.txt"});
+ assertThat("Force short switch should turn on force mode", parser.isForce(), is(true));
+ }
+
+ @Test
+ public void testParseForceModeSwitchLong() throws TxnLogToolkit.TxnLogToolkitParseException {
+ parser.parse(new String[] { "--yes", "txnlog.txt"});
+ assertThat("Force long switch should turn on force mode", parser.isForce(), is(true));
+ }
+}
diff --git a/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitTest.java b/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitTest.java
new file mode 100644
index 00000000000..62557d14676
--- /dev/null
+++ b/src/java/test/org/apache/zookeeper/server/persistence/TxnLogToolkitTest.java
@@ -0,0 +1,155 @@
+/**
+ * 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.zookeeper.server.persistence;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.zookeeper.test.ClientBase;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.matchers.JUnitMatchers.containsString;
+
+
+public class TxnLogToolkitTest {
+ private static final File testData = new File(
+ System.getProperty("test.data.dir", "build/test/data"));
+
+ private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ private File mySnapDir;
+
+ @Before
+ public void setUp() throws IOException {
+ System.setOut(new PrintStream(outContent));
+ System.setErr(new PrintStream(errContent));
+ File snapDir = new File(testData, "invalidsnap");
+ mySnapDir = ClientBase.createTmpDir();
+ FileUtils.copyDirectory(snapDir, mySnapDir);
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ System.setOut(System.out);
+ System.setErr(System.err);
+ mySnapDir.setWritable(true);
+ FileUtils.deleteDirectory(mySnapDir);
+ }
+
+ @Test
+ public void testDumpMode() throws Exception {
+ // Arrange
+ File logfile = new File(new File(mySnapDir, "version-2"), "log.274");
+ TxnLogToolkit lt = new TxnLogToolkit(false, false, logfile.toString(), true);
+
+ // Act
+ lt.dump(null);
+
+ // Assert
+ // no exception thrown
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitException.class)
+ public void testInitMissingFile() throws FileNotFoundException, TxnLogToolkit.TxnLogToolkitException {
+ // Arrange & Act
+ File logfile = new File("this_file_should_not_exists");
+ TxnLogToolkit lt = new TxnLogToolkit(false, false, logfile.toString(), true);
+ }
+
+ @Test(expected = TxnLogToolkit.TxnLogToolkitException.class)
+ public void testInitWithRecoveryFileExists() throws IOException, TxnLogToolkit.TxnLogToolkitException {
+ // Arrange & Act
+ File logfile = new File(new File(mySnapDir, "version-2"), "log.274");
+ File recoveryFile = new File(new File(mySnapDir, "version-2"), "log.274.fixed");
+ recoveryFile.createNewFile();
+ TxnLogToolkit lt = new TxnLogToolkit(true, false, logfile.toString(), true);
+ }
+
+ @Test
+ public void testDumpWithCrcError() throws Exception {
+ // Arrange
+ File logfile = new File(new File(mySnapDir, "version-2"), "log.42");
+ TxnLogToolkit lt = new TxnLogToolkit(false, false, logfile.toString(), true);
+
+ // Act
+ lt.dump(null);
+
+ // Assert
+ String output = outContent.toString();
+ Pattern p = Pattern.compile("^CRC ERROR.*session 0x8061fac5ddeb0000 cxid 0x0 zxid 0x8800000002 createSession 30000$", Pattern.MULTILINE);
+ Matcher m = p.matcher(output);
+ assertTrue("Output doesn't indicate CRC error for the broken session id: " + output, m.find());
+ }
+
+ @Test
+ public void testRecoveryFixBrokenFile() throws Exception {
+ // Arrange
+ File logfile = new File(new File(mySnapDir, "version-2"), "log.42");
+ TxnLogToolkit lt = new TxnLogToolkit(true, false, logfile.toString(), true);
+
+ // Act
+ lt.dump(null);
+
+ // Assert
+ String output = outContent.toString();
+ assertThat(output, containsString("CRC FIXED"));
+
+ // Should be able to dump the recovered logfile with no CRC error
+ outContent.reset();
+ logfile = new File(new File(mySnapDir, "version-2"), "log.42.fixed");
+ lt = new TxnLogToolkit(false, false, logfile.toString(), true);
+ lt.dump(null);
+ output = outContent.toString();
+ assertThat(output, not(containsString("CRC ERROR")));
+ }
+
+ @Test
+ public void testRecoveryInteractiveMode() throws Exception {
+ // Arrange
+ File logfile = new File(new File(mySnapDir, "version-2"), "log.42");
+ TxnLogToolkit lt = new TxnLogToolkit(true, false, logfile.toString(), false);
+
+ // Act
+ lt.dump(new StringReader("y\n"));
+
+ // Assert
+ String output = outContent.toString();
+ assertThat(output, containsString("CRC ERROR"));
+
+ // Should be able to dump the recovered logfile with no CRC error
+ outContent.reset();
+ logfile = new File(new File(mySnapDir, "version-2"), "log.42.fixed");
+ lt = new TxnLogToolkit(false, false, logfile.toString(), true);
+ lt.dump(null);
+ output = outContent.toString();
+ assertThat(output, not(containsString("CRC ERROR")));
+ }
+}
diff --git a/src/java/test/org/apache/zookeeper/test/ClientBase.java b/src/java/test/org/apache/zookeeper/test/ClientBase.java
index 74d0eed1107..7de4f3c3267 100644
--- a/src/java/test/org/apache/zookeeper/test/ClientBase.java
+++ b/src/java/test/org/apache/zookeeper/test/ClientBase.java
@@ -56,6 +56,7 @@
import org.apache.zookeeper.server.ServerCnxnFactoryAccessor;
import org.apache.zookeeper.server.ZKDatabase;
import org.apache.zookeeper.server.ZooKeeperServer;
+import org.apache.zookeeper.server.persistence.FilePadding;
import org.apache.zookeeper.server.persistence.FileTxnLog;
import org.apache.zookeeper.server.quorum.QuorumPeer;
import org.apache.zookeeper.server.util.OSMXBean;
@@ -467,7 +468,7 @@ public static void setupTestEnv() {
// resulting in test Assert.failure (client timeout on first session).
// set env and directly in order to handle static init/gc issues
System.setProperty("zookeeper.preAllocSize", "100");
- FileTxnLog.setPreallocSize(100 * 1024);
+ FilePadding.setPreallocSize(100 * 1024);
}
protected void setUpAll() throws Exception {