/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.file.internaldb;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.thrift.TBase;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.transport.TIOStreamTransport;
import org.diqube.file.internaldb.v1.SInternalDbFileHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Writer for files of "internal db" that writes and reads arbitrary collections of thrift {@link TBase}s.
*
* <p>
* InternalDb files do not only maintain a common file content, but also a common filename layout:
* prefix-commitIndex-suffix. The commitIndex is the index of the consensus cluster commit that initiated this write.
* This commitIndex is therefore increasing always, means higher index is newer file. The {@link InternalDbFileWriter}
* deletes old files.
*
* @author Bastian Gloeckle
*/
public class InternalDbFileWriter<T extends TBase<?, ?>> {
private static final Logger logger = LoggerFactory.getLogger(InternalDbFileWriter.class);
/** File version currently supported by {@link InternalDbFileWriter} and {@link InternalDbFileReader} */
/* package */ static final int VERSION = 1;
/* package */static final String FILENAME_SUFFIX = ".db";
private File outputDir;
private String dataType;
private String filenamePrefix;
public InternalDbFileWriter(String dataType, String filenamePrefix, File outputDir) {
this.dataType = dataType;
this.filenamePrefix = filenamePrefix;
this.outputDir = outputDir;
}
public void write(long consensusCommitIndex, List<T> entities) throws WriteException {
File[] filesToDelete =
outputDir.listFiles((dir, name) -> name.startsWith(filenamePrefix) && name.endsWith(FILENAME_SUFFIX));
File newFile = new File(outputDir, filenamePrefix + String.format("%020d", consensusCommitIndex) + FILENAME_SUFFIX);
logger.info("Writing updated {} entities to '{}'...", dataType, newFile.getAbsolutePath());
try (FileOutputStream fos = new FileOutputStream(newFile)) {
try (TIOStreamTransport transport = new TIOStreamTransport(fos)) {
TCompactProtocol protocol = new TCompactProtocol(transport);
SInternalDbFileHeader header = new SInternalDbFileHeader();
header.setVersion(VERSION);
header.setDataType(dataType);
header.setSize(entities.size());
header.write(protocol);
for (T e : entities) {
e.write(protocol);
}
logger.info("Updated internaldb file '{}'.", newFile.getAbsolutePath());
for (File f : filesToDelete) {
logger.info("Deleting old internaldb file '{}'", f.getAbsolutePath());
if (InternalDbFileUtil.parseCommitIndex(f, filenamePrefix, FILENAME_SUFFIX) > consensusCommitIndex)
// This could mean that consensus replays some commits during startup. That replay might be broken, if e.g.
// SerializationExceptions happen during the process and the classes of the serialized objects changed (=
// new version installed?).
logger.warn("Overwriting a presumably newer version of an {} internalDb file with an older version.",
dataType);
f.delete();
}
} catch (TException e) {
throw new WriteException("Could not write internaldb file '" + newFile.getAbsolutePath() + "'", e);
}
} catch (IOException e1) {
throw new WriteException("Could not write internaldb file '" + newFile.getAbsolutePath() + "'", e1);
}
}
public static class WriteException extends Exception {
private static final long serialVersionUID = 1L;
/* package */ WriteException(String msg) {
super(msg);
}
/* package */ WriteException(String msg, Throwable cause) {
super(msg, cause);
}
}
}