package io.eguan.dtx.journal;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* 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.
* #L%
*/
import static io.eguan.dtx.DtxConstants.DEFAULT_LAST_TX_VALUE;
import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Collections;
import java.util.NavigableMap;
import java.util.TreeMap;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.protobuf.InvalidProtocolBufferException;
/**
* Utility class for journal file operations.
*
* @author oodrive
* @author pwehrle
*
*/
public final class JournalFileUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(JournalFileUtils.class);
/**
* {@link FilenameFilter} for listing the journal file and all backups in a directory.
*
*
*/
static final class JournalFilenameFilter implements FilenameFilter {
private final String journalFilename;
/**
* Constructs an instance working with the provided journal filename.
*
* @param journalFilename
* a non-<code>null</code>, non-empty journal filename
*/
JournalFilenameFilter(@Nonnull final String journalFilename) {
if (Strings.isNullOrEmpty(journalFilename)) {
throw new IllegalArgumentException("Journal filename is null or empty");
}
this.journalFilename = journalFilename;
}
@Override
public final boolean accept(final File dir, final String name) {
return name.startsWith(journalFilename);
}
}
private JournalFileUtils() {
throw new AssertionError("Not instantiable.");
}
/**
* Extracts the rank of a backup file, i.e. the number after the last dot in the filename.
*
* @param baseName
* the journal base name to expect
* @param filename
* the filename of the backup file
* @return a positive backup number if one was found, 0 if the filename is the journal's filename itself and -1
* otherwise
*/
static final int extractBackupRank(final String baseName, final String filename) {
// gives the base file a rank of 0
if (baseName.equals(filename)) {
return 0;
}
// gives a rank of -1 to files without a dot or the last dot not immediately following the base file name
final int dotIndex = filename.lastIndexOf('.');
if ((dotIndex == -1) || (dotIndex > baseName.length())) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Not a backup; base=" + baseName + ", file=" + filename);
}
return -1;
}
// tries to extract a number from the filename part beyond the dot
// Note: may return 0 or negative numbers!
try {
return Integer.parseInt(filename.substring(dotIndex + 1));
}
catch (final IllegalArgumentException e) {
return -1;
}
}
/**
* Gets a map of backup files of a journal file from a given directory.
*
* @param journalDirectory
* the directory to search for backups
* @param journalFilename
* the base journal filename to filter by
* @return a (possibly empty) {@link Map<Integer,File>} of backup files sorted by inverse order of backup rank
* (highest first)
*/
static final NavigableMap<Integer, File> getInverseBackupMap(final File journalDirectory,
final String journalFilename) {
final File[] jFiles = journalDirectory.listFiles(new JournalFilenameFilter(journalFilename));
// inits a TreeMap that'll sort in reverse order
final TreeMap<Integer, File> result = new TreeMap<Integer, File>(Collections.reverseOrder());
if (jFiles == null) {
return result;
}
for (final File currFile : jFiles) {
final int backupRank = extractBackupRank(journalFilename, currFile.getName());
// don't add anything below 0 (ignores the journal file itself and everything that's not a valid backup)
if (backupRank > 0) {
result.put(Integer.valueOf(backupRank), currFile);
}
}
return result;
}
/**
* Reads the ID of the last completed transaction from the given journal.
*
* @param journal
* a non-<code>null</code> {@link Iterable} of {@link JournalRecord}
* @return a positive transaction ID read from the journal,
* {@value io.eguan.dtx.DtxConstants#DEFAULT_LAST_TX_VALUE} if none was found
*/
static final long readLastCompleteTxId(@Nonnull final Iterable<JournalRecord> journal) {
long result = DEFAULT_LAST_TX_VALUE;
// iterates on journal and decodes every entry
for (final JournalRecord currRecord : journal) {
try {
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
final long txId = currEntry.getTxId();
switch (currEntry.getOp()) {
case START:
break;
case COMMIT:
case ROLLBACK:
result = txId;
break;
default:
// nothing
}
}
catch (final InvalidProtocolBufferException e) {
throw new IllegalStateException(e);
}
}
return result;
}
}