Path: blob/master/src/java.rmi/share/classes/sun/rmi/log/ReliableLog.java
41155 views
/*1* Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package sun.rmi.log;2627import java.io.*;28import java.lang.reflect.Constructor;29import java.rmi.server.RMIClassLoader;30import java.security.AccessController;31import java.security.PrivilegedAction;3233/**34* This class is a simple implementation of a reliable Log. The35* client of a ReliableLog must provide a set of callbacks (via a36* LogHandler) that enables a ReliableLog to read and write37* checkpoints and log records. This implementation ensures that the38* current value of the data stored (via a ReliableLog) is recoverable39* after a system crash. <p>40*41* The secondary storage strategy is to record values in files using a42* representation of the caller's choosing. Two sorts of files are43* kept: snapshots and logs. At any instant, one snapshot is current.44* The log consists of a sequence of updates that have occurred since45* the current snapshot was taken. The current stable state is the46* value of the snapshot, as modified by the sequence of updates in47* the log. From time to time, the client of a ReliableLog instructs48* the package to make a new snapshot and clear the log. A ReliableLog49* arranges disk writes such that updates are stable (as long as the50* changes are force-written to disk) and atomic : no update is lost,51* and each update either is recorded completely in the log or not at52* all. Making a new snapshot is also atomic. <p>53*54* Normal use for maintaining the recoverable store is as follows: The55* client maintains the relevant data structure in virtual memory. As56* updates happen to the structure, the client informs the ReliableLog57* (all it "log") by calling log.update. Periodically, the client58* calls log.snapshot to provide the current value of the data59* structure. On restart, the client calls log.recover to obtain the60* latest snapshot and the following sequences of updates; the client61* applies the updates to the snapshot to obtain the state that62* existed before the crash. <p>63*64* The current logfile format is: <ol>65* <li> a format version number (two 4-octet integers, major and66* minor), followed by67* <li> a sequence of log records. Each log record contains, in68* order, <ol>69* <li> a 4-octet integer representing the length of the following log70* data,71* <li> the log data (variable length). </ol> </ol> <p>72*73* @see LogHandler74*75* @author Ann Wollrath76*77*/78public class ReliableLog {7980public final static int PreferredMajorVersion = 0;81public final static int PreferredMinorVersion = 2;8283// sun.rmi.log.debug=false84private boolean Debug = false;8586private static String snapshotPrefix = "Snapshot.";87private static String logfilePrefix = "Logfile.";88private static String versionFile = "Version_Number";89private static String newVersionFile = "New_Version_Number";90private static int intBytes = 4;91private static long diskPageSize = 512;9293private File dir; // base directory94private int version = 0; // current snapshot and log version95private String logName = null;96private LogFile log = null;97private long snapshotBytes = 0;98private long logBytes = 0;99private int logEntries = 0;100private long lastSnapshot = 0;101private long lastLog = 0;102//private long padBoundary = intBytes;103private LogHandler handler;104private final byte[] intBuf = new byte[4];105106// format version numbers read from/written to this.log107private int majorFormatVersion = 0;108private int minorFormatVersion = 0;109110111/**112* Constructor for the log file. If the system property113* sun.rmi.log.class is non-null and the class specified by this114* property a) can be loaded, b) is a subclass of LogFile, and c) has a115* public two-arg constructor (String, String), ReliableLog uses the116* constructor to construct the LogFile.117**/118private static final Constructor<? extends LogFile>119logClassConstructor = getLogClassConstructor();120121/**122* Creates a ReliableLog to handle checkpoints and logging in a123* stable storage directory.124*125* @param dirPath path to the stable storage directory126* @param handler the closure object containing callbacks for logging and127* recovery128* @param pad ignored129* @exception IOException If a directory creation error has130* occurred or if initialSnapshot callback raises an exception or131* if an exception occurs during invocation of the handler's132* snapshot method or if other IOException occurs.133*/134@SuppressWarnings("removal")135public ReliableLog(String dirPath,136LogHandler handler,137boolean pad)138throws IOException139{140super();141this.Debug = AccessController.doPrivileged(142(PrivilegedAction<Boolean>) () -> Boolean.getBoolean("sun.rmi.log.debug"));143dir = new File(dirPath);144if (!(dir.exists() && dir.isDirectory())) {145// create directory146if (!dir.mkdir()) {147throw new IOException("could not create directory for log: " +148dirPath);149}150}151//padBoundary = (pad ? diskPageSize : intBytes);152this.handler = handler;153lastSnapshot = 0;154lastLog = 0;155getVersion();156if (version == 0) {157try {158snapshot(handler.initialSnapshot());159} catch (IOException e) {160throw e;161} catch (Exception e) {162throw new IOException("initial snapshot failed with " +163"exception: " + e);164}165}166}167168/**169* Creates a ReliableLog to handle checkpoints and logging in a170* stable storage directory.171*172* @param dirPath path to the stable storage directory173* @param handler the closure object containing callbacks for logging and174* recovery175* @exception IOException If a directory creation error has176* occurred or if initialSnapshot callback raises an exception177*/178public ReliableLog(String dirPath,179LogHandler handler)180throws IOException181{182this(dirPath, handler, false);183}184185/* public methods */186187/**188* Returns an object which is the value recorded in the current189* snapshot. This snapshot is recovered by calling the client190* supplied callback "recover" and then subsequently invoking191* the "readUpdate" callback to apply any logged updates to the state.192*193* @exception IOException If recovery fails due to serious log194* corruption, read update failure, or if an exception occurs195* during the recover callback196*/197public synchronized Object recover()198throws IOException199{200if (Debug)201System.err.println("log.debug: recover()");202203if (version == 0)204return null;205206Object snapshot;207String fname = versionName(snapshotPrefix);208File snapshotFile = new File(fname);209InputStream in =210new BufferedInputStream(new FileInputStream(snapshotFile));211212if (Debug)213System.err.println("log.debug: recovering from " + fname);214215try {216try {217snapshot = handler.recover(in);218219} catch (IOException e) {220throw e;221} catch (Exception e) {222if (Debug)223System.err.println("log.debug: recovery failed: " + e);224throw new IOException("log recover failed with " +225"exception: " + e);226}227snapshotBytes = snapshotFile.length();228} finally {229in.close();230}231232return recoverUpdates(snapshot);233}234235/**236* Records this update in the log file (does not force update to disk).237* The update is recorded by calling the client's "writeUpdate" callback.238* This method must not be called until this log's recover method has239* been invoked (and completed).240*241* @param value the object representing the update242* @exception IOException If an exception occurred during a243* writeUpdate callback or if other I/O error has occurred.244*/245public synchronized void update(Object value) throws IOException {246update(value, true);247}248249/**250* Records this update in the log file. The update is recorded by251* calling the client's writeUpdate callback. This method must not be252* called until this log's recover method has been invoked253* (and completed).254*255* @param value the object representing the update256* @param forceToDisk ignored; changes are always forced to disk257* @exception IOException If force-write to log failed or an258* exception occurred during the writeUpdate callback or if other259* I/O error occurs while updating the log.260*/261public synchronized void update(Object value, boolean forceToDisk)262throws IOException263{264// avoid accessing a null log field.265if (log == null) {266throw new IOException("log is inaccessible, " +267"it may have been corrupted or closed");268}269270/*271* If the entry length field spans a sector boundary, write272* the high order bit of the entry length, otherwise write zero for273* the entry length.274*/275long entryStart = log.getFilePointer();276boolean spansBoundary = log.checkSpansBoundary(entryStart);277writeInt(log, spansBoundary? 1<<31 : 0);278279/*280* Write update, and sync.281*/282try {283handler.writeUpdate(new LogOutputStream(log), value);284} catch (IOException e) {285throw e;286} catch (Exception e) {287throw (IOException)288new IOException("write update failed").initCause(e);289}290log.sync();291292long entryEnd = log.getFilePointer();293int updateLen = (int) ((entryEnd - entryStart) - intBytes);294log.seek(entryStart);295296if (spansBoundary) {297/*298* If length field spans a sector boundary, then299* the next two steps are required (see 4652922):300*301* 1) Write actual length with high order bit set; sync.302* 2) Then clear high order bit of length; sync.303*/304writeInt(log, updateLen | 1<<31);305log.sync();306307log.seek(entryStart);308log.writeByte(updateLen >> 24);309log.sync();310311} else {312/*313* Write actual length; sync.314*/315writeInt(log, updateLen);316log.sync();317}318319log.seek(entryEnd);320logBytes = entryEnd;321lastLog = System.currentTimeMillis();322logEntries++;323}324325/**326* Returns the constructor for the log file if the system property327* sun.rmi.log.class is non-null and the class specified by the328* property a) can be loaded, b) is a subclass of LogFile, and c) has a329* public two-arg constructor (String, String); otherwise returns null.330**/331private static Constructor<? extends LogFile>332getLogClassConstructor() {333334@SuppressWarnings("removal")335String logClassName = AccessController.doPrivileged(336(PrivilegedAction<String>) () -> System.getProperty("sun.rmi.log.class"));337if (logClassName != null) {338try {339@SuppressWarnings("removal")340ClassLoader loader =341AccessController.doPrivileged(342new PrivilegedAction<ClassLoader>() {343public ClassLoader run() {344return ClassLoader.getSystemClassLoader();345}346});347Class<? extends LogFile> cl =348loader.loadClass(logClassName).asSubclass(LogFile.class);349return cl.getConstructor(String.class, String.class);350} catch (Exception e) {351System.err.println("Exception occurred:");352e.printStackTrace();353}354}355return null;356}357358/**359* Records this value as the current snapshot by invoking the client360* supplied "snapshot" callback and then empties the log.361*362* @param value the object representing the new snapshot363* @exception IOException If an exception occurred during the364* snapshot callback or if other I/O error has occurred during the365* snapshot process366*/367public synchronized void snapshot(Object value)368throws IOException369{370int oldVersion = version;371incrVersion();372373String fname = versionName(snapshotPrefix);374File snapshotFile = new File(fname);375FileOutputStream out = new FileOutputStream(snapshotFile);376try {377try {378handler.snapshot(out, value);379} catch (IOException e) {380throw e;381} catch (Exception e) {382throw new IOException("snapshot failed", e);383}384lastSnapshot = System.currentTimeMillis();385} finally {386out.close();387snapshotBytes = snapshotFile.length();388}389390openLogFile(true);391writeVersionFile(true);392commitToNewVersion();393deleteSnapshot(oldVersion);394deleteLogFile(oldVersion);395}396397/**398* Close the stable storage directory in an orderly manner.399*400* @exception IOException If an I/O error occurs when the log is401* closed402*/403public synchronized void close() throws IOException {404if (log == null) return;405try {406log.close();407} finally {408log = null;409}410}411412/**413* Returns the size of the snapshot file in bytes;414*/415public long snapshotSize() {416return snapshotBytes;417}418419/**420* Returns the size of the log file in bytes;421*/422public long logSize() {423return logBytes;424}425426/* private methods */427428/**429* Write an int value in single write operation. This method430* assumes that the caller is synchronized on the log file.431*432* @param out output stream433* @param val int value434* @throws IOException if any other I/O error occurs435*/436private void writeInt(DataOutput out, int val)437throws IOException438{439intBuf[0] = (byte) (val >> 24);440intBuf[1] = (byte) (val >> 16);441intBuf[2] = (byte) (val >> 8);442intBuf[3] = (byte) val;443out.write(intBuf);444}445446/**447* Generates a filename prepended with the stable storage directory path.448*449* @param name the leaf name of the file450*/451private String fName(String name) {452return dir.getPath() + File.separator + name;453}454455/**456* Generates a version 0 filename prepended with the stable storage457* directory path458*459* @param name version file name460*/461private String versionName(String name) {462return versionName(name, 0);463}464465/**466* Generates a version filename prepended with the stable storage467* directory path with the version number as a suffix.468*469* @param name version file name470* @thisversion a version number471*/472private String versionName(String prefix, int ver) {473ver = (ver == 0) ? version : ver;474return fName(prefix) + String.valueOf(ver);475}476477/**478* Increments the directory version number.479*/480private void incrVersion() {481do { version++; } while (version==0);482}483484/**485* Delete a file.486*487* @param name the name of the file488* @exception IOException If new version file couldn't be removed489*/490private void deleteFile(String name) throws IOException {491492File f = new File(name);493if (!f.delete())494throw new IOException("couldn't remove file: " + name);495}496497/**498* Removes the new version number file.499*500* @exception IOException If an I/O error has occurred.501*/502private void deleteNewVersionFile() throws IOException {503deleteFile(fName(newVersionFile));504}505506/**507* Removes the snapshot file.508*509* @param ver the version to remove510* @exception IOException If an I/O error has occurred.511*/512private void deleteSnapshot(int ver) throws IOException {513if (ver == 0) return;514deleteFile(versionName(snapshotPrefix, ver));515}516517/**518* Removes the log file.519*520* @param ver the version to remove521* @exception IOException If an I/O error has occurred.522*/523private void deleteLogFile(int ver) throws IOException {524if (ver == 0) return;525deleteFile(versionName(logfilePrefix, ver));526}527528/**529* Opens the log file in read/write mode. If file does not exist, it is530* created.531*532* @param truncate if true and file exists, file is truncated to zero533* length534* @exception IOException If an I/O error has occurred.535*/536private void openLogFile(boolean truncate) throws IOException {537try {538close();539} catch (IOException e) { /* assume this is okay */540}541542logName = versionName(logfilePrefix);543544try {545log = (logClassConstructor == null ?546new LogFile(logName, "rw") :547logClassConstructor.newInstance(logName, "rw"));548} catch (Exception e) {549throw (IOException) new IOException(550"unable to construct LogFile instance").initCause(e);551}552553if (truncate) {554initializeLogFile();555}556}557558/**559* Creates a new log file, truncated and initialized with the format560* version number preferred by this implementation.561* <p>Environment: inited, synchronized562* <p>Precondition: valid: log, log contains nothing useful563* <p>Postcondition: if successful, log is initialised with the format564* version number (Preferred{Major,Minor}Version), and logBytes is565* set to the resulting size of the updatelog, and logEntries is set to566* zero. Otherwise, log is in an indeterminate state, and logBytes567* is unchanged, and logEntries is unchanged.568*569* @exception IOException If an I/O error has occurred.570*/571private void initializeLogFile()572throws IOException573{574log.setLength(0);575majorFormatVersion = PreferredMajorVersion;576writeInt(log, PreferredMajorVersion);577minorFormatVersion = PreferredMinorVersion;578writeInt(log, PreferredMinorVersion);579logBytes = intBytes * 2;580logEntries = 0;581}582583584/**585* Writes out version number to file.586*587* @param newVersion if true, writes to a new version file588* @exception IOException If an I/O error has occurred.589*/590private void writeVersionFile(boolean newVersion) throws IOException {591String name;592if (newVersion) {593name = newVersionFile;594} else {595name = versionFile;596}597try (FileOutputStream fos = new FileOutputStream(fName(name));598DataOutputStream out = new DataOutputStream(fos)) {599writeInt(out, version);600}601}602603/**604* Creates the initial version file605*606* @exception IOException If an I/O error has occurred.607*/608private void createFirstVersion() throws IOException {609version = 0;610writeVersionFile(false);611}612613/**614* Commits (atomically) the new version.615*616* @exception IOException If an I/O error has occurred.617*/618private void commitToNewVersion() throws IOException {619writeVersionFile(false);620deleteNewVersionFile();621}622623/**624* Reads version number from a file.625*626* @param name the name of the version file627* @return the version628* @exception IOException If an I/O error has occurred.629*/630private int readVersion(String name) throws IOException {631try (DataInputStream in = new DataInputStream632(new FileInputStream(name))) {633return in.readInt();634}635}636637/**638* Sets the version. If version file does not exist, the initial639* version file is created.640*641* @exception IOException If an I/O error has occurred.642*/643private void getVersion() throws IOException {644try {645version = readVersion(fName(newVersionFile));646commitToNewVersion();647} catch (IOException e) {648try {649deleteNewVersionFile();650}651catch (IOException ex) {652}653654try {655version = readVersion(fName(versionFile));656}657catch (IOException ex) {658createFirstVersion();659}660}661}662663/**664* Applies outstanding updates to the snapshot.665*666* @param state the most recent snapshot667* @exception IOException If serious log corruption is detected or668* if an exception occurred during a readUpdate callback or if669* other I/O error has occurred.670* @return the resulting state of the object after all updates671*/672private Object recoverUpdates(Object state)673throws IOException674{675logBytes = 0;676logEntries = 0;677678if (version == 0) return state;679680String fname = versionName(logfilePrefix);681InputStream in =682new BufferedInputStream(new FileInputStream(fname));683DataInputStream dataIn = new DataInputStream(in);684685if (Debug)686System.err.println("log.debug: reading updates from " + fname);687688try {689majorFormatVersion = dataIn.readInt(); logBytes += intBytes;690minorFormatVersion = dataIn.readInt(); logBytes += intBytes;691} catch (EOFException e) {692/* This is a log which was corrupted and/or cleared (by693* fsck or equivalent). This is not an error.694*/695openLogFile(true); // create and truncate696in = null;697}698/* A new major version number is a catastrophe (it means699* that the file format is incompatible with older700* clients, and we'll only be breaking things by trying to701* use the log). A new minor version is no big deal for702* upward compatibility.703*/704if (majorFormatVersion != PreferredMajorVersion) {705if (Debug) {706System.err.println("log.debug: major version mismatch: " +707majorFormatVersion + "." + minorFormatVersion);708}709throw new IOException("Log file " + logName + " has a " +710"version " + majorFormatVersion +711"." + minorFormatVersion +712" format, and this implementation " +713" understands only version " +714PreferredMajorVersion + "." +715PreferredMinorVersion);716}717718try {719while (in != null) {720int updateLen = 0;721722try {723updateLen = dataIn.readInt();724} catch (EOFException e) {725if (Debug)726System.err.println("log.debug: log was sync'd cleanly");727break;728}729if (updateLen <= 0) {/* crashed while writing last log entry */730if (Debug) {731System.err.println(732"log.debug: last update incomplete, " +733"updateLen = 0x" +734Integer.toHexString(updateLen));735}736break;737}738739// this is a fragile use of available() which relies on the740// twin facts that BufferedInputStream correctly consults741// the underlying stream, and that FileInputStream returns742// the number of bytes remaining in the file (via FIONREAD).743if (in.available() < updateLen) {744/* corrupted record at end of log (can happen since we745* do only one fsync)746*/747if (Debug)748System.err.println("log.debug: log was truncated");749break;750}751752if (Debug)753System.err.println("log.debug: rdUpdate size " + updateLen);754try {755state = handler.readUpdate(new LogInputStream(in, updateLen),756state);757} catch (IOException e) {758throw e;759} catch (Exception e) {760e.printStackTrace();761throw new IOException("read update failed with " +762"exception: " + e);763}764logBytes += (intBytes + updateLen);765logEntries++;766} /* while */767} finally {768if (in != null)769in.close();770}771772if (Debug)773System.err.println("log.debug: recovered updates: " + logEntries);774775/* reopen log file at end */776openLogFile(false);777778// avoid accessing a null log field779if (log == null) {780throw new IOException("rmid's log is inaccessible, " +781"it may have been corrupted or closed");782}783784log.seek(logBytes);785log.setLength(logBytes);786787return state;788}789790/**791* ReliableLog's log file implementation. This implementation792* is subclassable for testing purposes.793*/794public static class LogFile extends RandomAccessFile {795796private final FileDescriptor fd;797798/**799* Constructs a LogFile and initializes the file descriptor.800**/801public LogFile(String name, String mode)802throws FileNotFoundException, IOException803{804super(name, mode);805this.fd = getFD();806}807808/**809* Invokes sync on the file descriptor for this log file.810*/811protected void sync() throws IOException {812fd.sync();813}814815/**816* Returns true if writing 4 bytes starting at the specified file817* position, would span a 512 byte sector boundary; otherwise returns818* false.819**/820protected boolean checkSpansBoundary(long fp) {821return fp % 512 > 508;822}823}824}825826827