/*
* 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 com.sun.jini.reliableLog;
import java.io.*;
/**
* This class is a simple implementation of a reliable Log. The
* client of a ReliableLog must provide a set of callbacks (via a
* LogHandler) that enables a ReliableLog to read and write snapshots
* (checkpoints) and log records. This implementation ensures that the
* data stored (via a ReliableLog) is recoverable after a system crash.
* The implementation is unsynchronized; the client must synchronize
* externally. <p>
*
* The secondary storage strategy is to record values in files using a
* representation of the caller's choosing. Two sorts of files are
* kept: snapshots and logs. At any instant, one snapshot is current.
* The log consists of a sequence of updates that have occurred since
* the current snapshot was taken. The current stable state is the
* value of the snapshot, as modified by the sequence of updates in
* the log. From time to time, the client of a ReliableLog instructs
* the package to make a new snapshot and clear the log. A ReliableLog
* arranges disk writes such that updates are stable (as long as the
* changes are force-written to disk) and atomic: no update is lost,
* and each update either is recorded completely in the log or not at
* all. Making a new snapshot is also atomic. <p>
*
* Normal use for maintaining the recoverable store is as follows: The
* client maintains the relevant data structure in virtual memory. As
* updates happen to the structure, the client informs the ReliableLog
* (call it "log") by calling log.update. Periodically, the client
* calls log.snapshot to provide the current complete contents of the
* data. On restart, the client calls log.recover to obtain the
* latest snapshot and the following sequences of updates; the client
* applies the updates to the snapshot to obtain the state that
* existed before the crash. <p>
*
* @author Sun Microsystems, Inc.
*
* @see LogHandler
*
*/
public class ReliableLog {
private static final String snapshotPrefix = "Snapshot.";
private static final String logfilePrefix = "Logfile.";
private static final String versionFile = "Version_Number";
private static final int MAGIC = 0xf2ecefe7;
private static final int FORMAT_UNPADDED = 0;
private static final int FORMAT_PADDED = 1;
private static final long intBytes = 4;
private final File dir; // base directory
private int version = 0; // current snapshot and log version
private int format = FORMAT_UNPADDED;
private String logName = null;
private RandomAccessFile log = null;
private FileDescriptor logFD;
private long snapshotBytes = 0;
private long logBytes = 0;
private final LogHandler handler;
private final byte[] intBuf = new byte[4];
private final byte[] zeroBuf = new byte[4];
/**
* Creates a ReliableLog to handle snapshots and logging in a
* stable storage directory, and sets up to recover any existing data
* from the stable storage directory. If there is no existing data,
* snapshot must be called next, otherwise recover must be called next.
*
* @param dirPath path to the stable storage directory
* @param handler the handler for log callbacks
*
* @throws LogException if the directory cannot be created or
* the current version in the directory is corrupted
* @throws IOException if any other I/O error occurs
*/
public ReliableLog(String dirPath, LogHandler handler) throws IOException {
dir = new File(dirPath);
if (!(dir.exists() ? dir.isDirectory() : dir.mkdir())) {
throw new LogException("could not create directory for log: " +
dirPath);
}
this.handler = handler;
try {
DataInputStream in =
new DataInputStream(new FileInputStream(fName(versionFile)));
try {
version = in.readInt();
} finally {
in.close();
}
} catch (IOException ex) {
writeVersionFile();
}
if (version < 0) {
throw new LogException("corrupted version file");
}
}
/**
* Retrieves the contents of the snapshot file by calling the client
* supplied recover callback and then applies the incremental updates
* by calling the readUpdate callback for each logged updated.
*
* @throws LogException if recovery fails due to serious log corruption,
* or if an exception is thrown by the recover or readUpdate callbacks
* @throws IOException if an other I/O error occurs
*/
public void recover() throws IOException {
if (version == 0)
return;
String fname = versionName(snapshotPrefix);
File file = new File(fname);
InputStream in = new BufferedInputStream(new FileInputStream(file));
try {
handler.recover(in);
} catch (Exception e) {
throw new LogException("recovery failed", e);
} finally {
in.close();
}
snapshotBytes = file.length();
fname = versionName(logfilePrefix);
file = new File(fname);
DataInputStream din =
new DataInputStream(new BufferedInputStream(
new FileInputStream(file)));
long length = file.length();
try {
int updateLen = din.readInt();
/* have to worry about no MAGIC in original format */
if (updateLen == MAGIC) {
format = din.readInt();
if (format != FORMAT_PADDED) {
throw new LogException("corrupted log: bad log format");
}
logBytes += (intBytes + intBytes);
updateLen = din.readInt();
}
while (true) {
if (updateLen == 0) { /* expected termination case */
break;
}
if (updateLen < 0) { /* serious corruption */
throw new LogException("corrupted log: bad update length");
}
if (length - logBytes - intBytes < updateLen) {
/* partial record at end of log; this should not happen
* if forceToDisk is always true, but might happen if
* buffered updates are used.
*/
break;
}
try {
handler.readUpdate(new LogInputStream(din, updateLen));
} catch (Exception e) {
throw new LogException("read update failed", e);
}
logBytes += (intBytes + updateLen);
if (format == FORMAT_PADDED) {
int offset = (int)logBytes & 3;
if (offset > 0) {
offset = 4 - offset;
logBytes += offset;
din.skipBytes(offset);
}
}
updateLen = din.readInt();
} /* while */
} catch (EOFException e) {
} finally {
din.close();
}
/* reopen log file at end */
openLogFile();
}
/**
* Records this update in the log file and forces the update to disk.
* The update is recorded by calling the client's writeUpdate callback.
* This method must not be called until this log's recover method has
* been invoked (and completed).
*
* @param value the object representing the update
*
* @throws LogException if an exception is thrown by the writeUpdate
* callback, or forcing the update to disk fails
* @throws IOException if any other I/O error occurs
*/
public void update(Object value) throws IOException {
update(value, true);
}
/**
* Records this update in the log file and optionally forces the update
* to disk. The update is recorded by calling the client's writeUpdate
* callback. This method must not be called until this log's recover
* method has been invoked (and completed).
*
* @param value the object representing the update
* @param forceToDisk true if the update should be forced to disk, false
* if the updates should be buffered
*
* @throws LogException if an exception is thrown by the writeUpdate
* callback, or forcing the update to disk fails
* @throws IOException if any other I/O error occurs
*/
public void update(Object value, boolean forceToDisk) throws IOException {
/* avoid accessing a null log field */
if (log == null) {
throw new LogException("log file for persistent state is "
+"inaccessible, it may have been "
+"corrupted or closed");
}
/* note: zero length header for this update was written as part
* of the previous update, or at initial opening of the log file
*/
try {
handler.writeUpdate(new LogOutputStream(log), value);
} catch (Exception e) {
throw new LogException("write update failed", e);
}
if (forceToDisk) {
/* must force contents to disk before writing real length header */
try {
logFD.sync();
} catch (SyncFailedException sfe) {
throw new LogException("sync log failed", sfe);
}
}
long entryEnd = log.getFilePointer();
long updateLen = entryEnd - logBytes - intBytes;
if (updateLen > Integer.MAX_VALUE) {
throw new LogException("maximum record length exceeded");
}
/* write real length header */
log.seek(logBytes);
writeInt(log, (int) updateLen);
/* pad out update record so length header does not span disk blocks */
if (format == FORMAT_PADDED) {
entryEnd = (entryEnd + 3) & ~3L;
}
/* write zero length header for next update */
log.seek(entryEnd);
log.write(zeroBuf);
logBytes = entryEnd;
/* force both length headers to disk */
if (forceToDisk) {
try {
logFD.sync();
} catch (SyncFailedException sfe) {
throw new LogException("sync log failed", sfe);
}
}
}
/**
* Write an int value in single write operation.
*
* @param out output stream
* @param val int value
* @throws IOException if any other I/O error occurs
*/
private void writeInt(DataOutput out, int val) throws IOException {
intBuf[0] = (byte) (val >> 24);
intBuf[1] = (byte) (val >> 16);
intBuf[2] = (byte) (val >> 8);
intBuf[3] = (byte) val;
out.write(intBuf);
}
/**
* Records the client-defined current snapshot by invoking the client
* supplied snapshot callback, and then empties the log of incremental
* updates.
*
* @throws LogException if the snapshot callback throws an exception
* @throws IOException if any other I/O error occurs
*/
public void snapshot() throws IOException {
int oldVersion = version;
version++;
String fname = versionName(snapshotPrefix);
File snapshotFile = new File(fname);
FileOutputStream out = new FileOutputStream(snapshotFile);
try {
try {
handler.snapshot(out);
/* force contents to disk */
out.getFD().sync();
} catch (Exception e) {
throw new LogException("snapshot failed", e);
}
snapshotBytes = snapshotFile.length();
} finally {
out.close();
}
logBytes = 0;
openLogFile();
writeVersionFile();
deleteSnapshot(oldVersion);
deleteLogFile(oldVersion);
}
/**
* Closes the stable storage directory in an orderly manner.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (log == null) return;
try {
log.close();
} finally {
log = null;
}
}
/**
* Closes the incremental update log file, removes all ReliableLog-related
* files from the stable storage directory, and deletes the directory.
*/
public void deletePersistentStore() {
try {
close();
} catch (IOException e) {
}
try {
deleteLogFile(version);
} catch (LogException e) {
}
try {
deleteSnapshot(version);
} catch (LogException e) {
}
try {
deleteFile(fName(versionFile));
} catch (LogException e) {
}
try {
/* Delete the directory. The following call to the delete method
* will fail only if the directory is not empty or if the Security
* Manager's checkDelete() method throws a SecurityException.
* (The Security Manager will throw such an exception if it
* determines that the current application is not allowed to
* delete the directory.) For either case, upon un-successful
* deletion of the directory, take no further action.
*/
dir.delete();
} catch (SecurityException e) {
}
}
/**
* Returns the size of the current snapshot file in bytes;
*/
public long snapshotSize() { return snapshotBytes; }
/**
* Returns the current size of the incremental update log file in bytes;
*/
public long logSize() { return logBytes; }
/**
* Generates a filename prepended with the stable storage directory path.
*
* @param name the name of the file (sans directory path)
*/
private String fName(String name) {
return dir.getPath() + File.separator + name;
}
/**
* Generates a version filename prepended with the stable storage
* directory path with the current version number as a suffix.
*
* @param name version filename prefix
*/
private String versionName(String name) {
return versionName(name, version);
}
/**
* Generates a version filename prepended with the stable storage
* directory path with the given version number as a suffix.
*
* @param version filename prefix
* @param ver version number
*/
private String versionName(String prefix, int ver) {
return fName(prefix) + String.valueOf(ver);
}
/**
* Deletes a file.
*
* @param name the name of the file (complete path)
* @throws LogException if file cannot be deleted
*/
private void deleteFile(String name) throws LogException {
if (!new File(name).delete()) {
throw new LogException("couldn't delete file: " + name);
}
}
/**
* Removes the snapshot file.
*
* @param ver the version to remove
*
* @throws LogException if file cannot be deleted
*/
private void deleteSnapshot(int ver) throws LogException {
if (ver != 0) {
deleteFile(versionName(snapshotPrefix, ver));
}
}
/**
* Removes the incremental update log file.
*
* @param ver the version to remove
*
* @throws LogException if file cannot be deleted
*/
private void deleteLogFile(int ver) throws LogException {
if (ver != 0) {
deleteFile(versionName(logfilePrefix, ver));
}
}
/**
* Opens the incremental update log file in read/write mode. If the
* file does not exist, it is created.
*
* @throws IOException if an I/O error occurs
*/
private void openLogFile() throws IOException {
try {
close();
} catch (IOException e) { /* assume this is okay */
}
logName = versionName(logfilePrefix);
log = new RandomAccessFile(logName, "rw");
logFD = log.getFD();
if (logBytes == 0) {
format = FORMAT_PADDED;
writeInt(log, MAGIC);
writeInt(log, format);
logBytes = (intBytes + intBytes);
} else {
log.seek(logBytes);
}
log.setLength(logBytes);
/* always start out with zero length header for the next update */
log.write(zeroBuf);
/* force length header to disk */
logFD.sync();
}
/**
* Writes the current version number to the version file.
*
* @throws IOException if an I/O error occurs
*/
private void writeVersionFile() throws IOException {
RandomAccessFile out = new RandomAccessFile(fName(versionFile), "rw");
try {
/* write should be atomic (four bytes on one disk block) */
writeInt(out, version);
/* force version to disk */
out.getFD().sync();
} finally {
out.close();
}
}
}