1/*
2 * Copyright (c) 1997, 2012, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.rmi.log;
27
28import java.io.*;
29import java.lang.reflect.Constructor;
30import java.rmi.server.RMIClassLoader;
31import java.security.AccessController;
32import java.security.PrivilegedAction;
33
34/**
35 * This class is a simple implementation of a reliable Log.  The
36 * client of a ReliableLog must provide a set of callbacks (via a
37 * LogHandler) that enables a ReliableLog to read and write
38 * checkpoints and log records.  This implementation ensures that the
39 * current value of the data stored (via a ReliableLog) is recoverable
40 * after a system crash. <p>
41 *
42 * The secondary storage strategy is to record values in files using a
43 * representation of the caller's choosing.  Two sorts of files are
44 * kept: snapshots and logs.  At any instant, one snapshot is current.
45 * The log consists of a sequence of updates that have occurred since
46 * the current snapshot was taken.  The current stable state is the
47 * value of the snapshot, as modified by the sequence of updates in
48 * the log.  From time to time, the client of a ReliableLog instructs
49 * the package to make a new snapshot and clear the log.  A ReliableLog
50 * arranges disk writes such that updates are stable (as long as the
51 * changes are force-written to disk) and atomic : no update is lost,
52 * and each update either is recorded completely in the log or not at
53 * all.  Making a new snapshot is also atomic. <p>
54 *
55 * Normal use for maintaining the recoverable store is as follows: The
56 * client maintains the relevant data structure in virtual memory.  As
57 * updates happen to the structure, the client informs the ReliableLog
58 * (all it "log") by calling log.update.  Periodically, the client
59 * calls log.snapshot to provide the current value of the data
60 * structure.  On restart, the client calls log.recover to obtain the
61 * latest snapshot and the following sequences of updates; the client
62 * applies the updates to the snapshot to obtain the state that
63 * existed before the crash. <p>
64 *
65 * The current logfile format is: <ol>
66 * <li> a format version number (two 4-octet integers, major and
67 * minor), followed by
68 * <li> a sequence of log records.  Each log record contains, in
69 * order, <ol>
70 * <li> a 4-octet integer representing the length of the following log
71 * data,
72 * <li> the log data (variable length). </ol> </ol> <p>
73 *
74 * @see LogHandler
75 *
76 * @author Ann Wollrath
77 *
78 */
79public class ReliableLog {
80
81    public final static int PreferredMajorVersion = 0;
82    public final static int PreferredMinorVersion = 2;
83
84    // sun.rmi.log.debug=false
85    private boolean Debug = false;
86
87    private static String snapshotPrefix = "Snapshot.";
88    private static String logfilePrefix = "Logfile.";
89    private static String versionFile = "Version_Number";
90    private static String newVersionFile = "New_Version_Number";
91    private static int    intBytes = 4;
92    private static long   diskPageSize = 512;
93
94    private File dir;                   // base directory
95    private int version = 0;            // current snapshot and log version
96    private String logName = null;
97    private LogFile log = null;
98    private long snapshotBytes = 0;
99    private long logBytes = 0;
100    private int logEntries = 0;
101    private long lastSnapshot = 0;
102    private long lastLog = 0;
103    //private long padBoundary = intBytes;
104    private LogHandler handler;
105    private final byte[] intBuf = new byte[4];
106
107    // format version numbers read from/written to this.log
108    private int majorFormatVersion = 0;
109    private int minorFormatVersion = 0;
110
111
112    /**
113     * Constructor for the log file.  If the system property
114     * sun.rmi.log.class is non-null and the class specified by this
115     * property a) can be loaded, b) is a subclass of LogFile, and c) has a
116     * public two-arg constructor (String, String), ReliableLog uses the
117     * constructor to construct the LogFile.
118     **/
119    private static final Constructor<? extends LogFile>
120        logClassConstructor = getLogClassConstructor();
121
122    /**
123     * Creates a ReliableLog to handle checkpoints and logging in a
124     * stable storage directory.
125     *
126     * @param dirPath path to the stable storage directory
127     * @param handler the closure object containing callbacks for logging and
128     * recovery
129     * @param pad ignored
130     * @exception IOException If a directory creation error has
131     * occurred or if initialSnapshot callback raises an exception or
132     * if an exception occurs during invocation of the handler's
133     * snapshot method or if other IOException occurs.
134     */
135    public ReliableLog(String dirPath,
136                     LogHandler handler,
137                     boolean pad)
138        throws IOException
139    {
140        super();
141        this.Debug = AccessController.doPrivileged(
142            (PrivilegedAction<Boolean>) () -> Boolean.getBoolean("sun.rmi.log.debug"));
143        dir = new File(dirPath);
144        if (!(dir.exists() && dir.isDirectory())) {
145            // create directory
146            if (!dir.mkdir()) {
147                throw new IOException("could not create directory for log: " +
148                                      dirPath);
149            }
150        }
151        //padBoundary = (pad ? diskPageSize : intBytes);
152        this.handler = handler;
153        lastSnapshot = 0;
154        lastLog = 0;
155        getVersion();
156        if (version == 0) {
157            try {
158                snapshot(handler.initialSnapshot());
159            } catch (IOException e) {
160                throw e;
161            } catch (Exception e) {
162                throw new IOException("initial snapshot failed with " +
163                                      "exception: " + e);
164            }
165        }
166    }
167
168    /**
169     * Creates a ReliableLog to handle checkpoints and logging in a
170     * stable storage directory.
171     *
172     * @param dirPath path to the stable storage directory
173     * @param handler the closure object containing callbacks for logging and
174     *        recovery
175     * @exception IOException If a directory creation error has
176     * occurred or if initialSnapshot callback raises an exception
177     */
178    public ReliableLog(String dirPath,
179                     LogHandler handler)
180        throws IOException
181    {
182        this(dirPath, handler, false);
183    }
184
185    /* public methods */
186
187    /**
188     * Returns an object which is the value recorded in the current
189     * snapshot.  This snapshot is recovered by calling the client
190     * supplied callback "recover" and then subsequently invoking
191     * the "readUpdate" callback to apply any logged updates to the state.
192     *
193     * @exception IOException If recovery fails due to serious log
194     * corruption, read update failure, or if an exception occurs
195     * during the recover callback
196     */
197    public synchronized Object recover()
198        throws IOException
199    {
200        if (Debug)
201            System.err.println("log.debug: recover()");
202
203        if (version == 0)
204            return null;
205
206        Object snapshot;
207        String fname = versionName(snapshotPrefix);
208        File snapshotFile = new File(fname);
209        InputStream in =
210                new BufferedInputStream(new FileInputStream(snapshotFile));
211
212        if (Debug)
213            System.err.println("log.debug: recovering from " + fname);
214
215        try {
216            try {
217                snapshot = handler.recover(in);
218
219            } catch (IOException e) {
220                throw e;
221            } catch (Exception e) {
222                if (Debug)
223                    System.err.println("log.debug: recovery failed: " + e);
224                throw new IOException("log recover failed with " +
225                                      "exception: " + e);
226            }
227            snapshotBytes = snapshotFile.length();
228        } finally {
229            in.close();
230        }
231
232        return recoverUpdates(snapshot);
233    }
234
235    /**
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 has
239     * been invoked (and completed).
240     *
241     * @param value the object representing the update
242     * @exception IOException If an exception occurred during a
243     * writeUpdate callback or if other I/O error has occurred.
244     */
245    public synchronized void update(Object value) throws IOException {
246        update(value, true);
247    }
248
249    /**
250     * Records this update in the log file.  The update is recorded by
251     * calling the client's writeUpdate callback.  This method must not be
252     * called until this log's recover method has been invoked
253     * (and completed).
254     *
255     * @param value the object representing the update
256     * @param forceToDisk ignored; changes are always forced to disk
257     * @exception IOException If force-write to log failed or an
258     * exception occurred during the writeUpdate callback or if other
259     * I/O error occurs while updating the log.
260     */
261    public synchronized void update(Object value, boolean forceToDisk)
262        throws IOException
263    {
264        // avoid accessing a null log field.
265        if (log == null) {
266            throw new IOException("log is inaccessible, " +
267                "it may have been corrupted or closed");
268        }
269
270        /*
271         * If the entry length field spans a sector boundary, write
272         * the high order bit of the entry length, otherwise write zero for
273         * the entry length.
274         */
275        long entryStart = log.getFilePointer();
276        boolean spansBoundary = log.checkSpansBoundary(entryStart);
277        writeInt(log, spansBoundary? 1<<31 : 0);
278
279        /*
280         * Write update, and sync.
281         */
282        try {
283            handler.writeUpdate(new LogOutputStream(log), value);
284        } catch (IOException e) {
285            throw e;
286        } catch (Exception e) {
287            throw (IOException)
288                new IOException("write update failed").initCause(e);
289        }
290        log.sync();
291
292        long entryEnd = log.getFilePointer();
293        int updateLen = (int) ((entryEnd - entryStart) - intBytes);
294        log.seek(entryStart);
295
296        if (spansBoundary) {
297            /*
298             * If length field spans a sector boundary, then
299             * 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             */
304            writeInt(log, updateLen | 1<<31);
305            log.sync();
306
307            log.seek(entryStart);
308            log.writeByte(updateLen >> 24);
309            log.sync();
310
311        } else {
312            /*
313             * Write actual length; sync.
314             */
315            writeInt(log, updateLen);
316            log.sync();
317        }
318
319        log.seek(entryEnd);
320        logBytes = entryEnd;
321        lastLog = System.currentTimeMillis();
322        logEntries++;
323    }
324
325    /**
326     * Returns the constructor for the log file if the system property
327     * sun.rmi.log.class is non-null and the class specified by the
328     * property a) can be loaded, b) is a subclass of LogFile, and c) has a
329     * public two-arg constructor (String, String); otherwise returns null.
330     **/
331    private static Constructor<? extends LogFile>
332        getLogClassConstructor() {
333
334        String logClassName = AccessController.doPrivileged(
335            (PrivilegedAction<String>) () -> System.getProperty("sun.rmi.log.class"));
336        if (logClassName != null) {
337            try {
338                ClassLoader loader =
339                    AccessController.doPrivileged(
340                        new PrivilegedAction<ClassLoader>() {
341                            public ClassLoader run() {
342                               return ClassLoader.getSystemClassLoader();
343                            }
344                        });
345                Class<? extends LogFile> cl =
346                    loader.loadClass(logClassName).asSubclass(LogFile.class);
347                return cl.getConstructor(String.class, String.class);
348            } catch (Exception e) {
349                System.err.println("Exception occurred:");
350                e.printStackTrace();
351            }
352        }
353        return null;
354    }
355
356    /**
357     * Records this value as the current snapshot by invoking the client
358     * supplied "snapshot" callback and then empties the log.
359     *
360     * @param value the object representing the new snapshot
361     * @exception IOException If an exception occurred during the
362     * snapshot callback or if other I/O error has occurred during the
363     * snapshot process
364     */
365    public synchronized void snapshot(Object value)
366        throws IOException
367    {
368        int oldVersion = version;
369        incrVersion();
370
371        String fname = versionName(snapshotPrefix);
372        File snapshotFile = new File(fname);
373        FileOutputStream out = new FileOutputStream(snapshotFile);
374        try {
375            try {
376                handler.snapshot(out, value);
377            } catch (IOException e) {
378                throw e;
379            } catch (Exception e) {
380                throw new IOException("snapshot failed", e);
381            }
382            lastSnapshot = System.currentTimeMillis();
383        } finally {
384            out.close();
385            snapshotBytes = snapshotFile.length();
386        }
387
388        openLogFile(true);
389        writeVersionFile(true);
390        commitToNewVersion();
391        deleteSnapshot(oldVersion);
392        deleteLogFile(oldVersion);
393    }
394
395    /**
396     * Close the stable storage directory in an orderly manner.
397     *
398     * @exception IOException If an I/O error occurs when the log is
399     * closed
400     */
401    public synchronized void close() throws IOException {
402        if (log == null) return;
403        try {
404            log.close();
405        } finally {
406            log = null;
407        }
408    }
409
410    /**
411     * Returns the size of the snapshot file in bytes;
412     */
413    public long snapshotSize() {
414        return snapshotBytes;
415    }
416
417    /**
418     * Returns the size of the log file in bytes;
419     */
420    public long logSize() {
421        return logBytes;
422    }
423
424    /* private methods */
425
426    /**
427     * Write an int value in single write operation.  This method
428     * assumes that the caller is synchronized on the log file.
429     *
430     * @param out output stream
431     * @param val int value
432     * @throws IOException if any other I/O error occurs
433     */
434    private void writeInt(DataOutput out, int val)
435        throws IOException
436    {
437        intBuf[0] = (byte) (val >> 24);
438        intBuf[1] = (byte) (val >> 16);
439        intBuf[2] = (byte) (val >> 8);
440        intBuf[3] = (byte) val;
441        out.write(intBuf);
442    }
443
444    /**
445     * Generates a filename prepended with the stable storage directory path.
446     *
447     * @param name the leaf name of the file
448     */
449    private String fName(String name) {
450        return dir.getPath() + File.separator + name;
451    }
452
453    /**
454     * Generates a version 0 filename prepended with the stable storage
455     * directory path
456     *
457     * @param name version file name
458     */
459    private String versionName(String name) {
460        return versionName(name, 0);
461    }
462
463    /**
464     * Generates a version filename prepended with the stable storage
465     * directory path with the version number as a suffix.
466     *
467     * @param name version file name
468     * @thisversion a version number
469     */
470    private String versionName(String prefix, int ver) {
471        ver = (ver == 0) ? version : ver;
472        return fName(prefix) + String.valueOf(ver);
473    }
474
475    /**
476     * Increments the directory version number.
477     */
478    private void incrVersion() {
479        do { version++; } while (version==0);
480    }
481
482    /**
483     * Delete a file.
484     *
485     * @param name the name of the file
486     * @exception IOException If new version file couldn't be removed
487     */
488    private void deleteFile(String name) throws IOException {
489
490        File f = new File(name);
491        if (!f.delete())
492            throw new IOException("couldn't remove file: " + name);
493    }
494
495    /**
496     * Removes the new version number file.
497     *
498     * @exception IOException If an I/O error has occurred.
499     */
500    private void deleteNewVersionFile() throws IOException {
501        deleteFile(fName(newVersionFile));
502    }
503
504    /**
505     * Removes the snapshot file.
506     *
507     * @param ver the version to remove
508     * @exception IOException If an I/O error has occurred.
509     */
510    private void deleteSnapshot(int ver) throws IOException {
511        if (ver == 0) return;
512        deleteFile(versionName(snapshotPrefix, ver));
513    }
514
515    /**
516     * Removes the log file.
517     *
518     * @param ver the version to remove
519     * @exception IOException If an I/O error has occurred.
520     */
521    private void deleteLogFile(int ver) throws IOException {
522        if (ver == 0) return;
523        deleteFile(versionName(logfilePrefix, ver));
524    }
525
526    /**
527     * Opens the log file in read/write mode.  If file does not exist, it is
528     * created.
529     *
530     * @param truncate if true and file exists, file is truncated to zero
531     * length
532     * @exception IOException If an I/O error has occurred.
533     */
534    private void openLogFile(boolean truncate) throws IOException {
535        try {
536            close();
537        } catch (IOException e) { /* assume this is okay */
538        }
539
540        logName = versionName(logfilePrefix);
541
542        try {
543            log = (logClassConstructor == null ?
544                   new LogFile(logName, "rw") :
545                   logClassConstructor.newInstance(logName, "rw"));
546        } catch (Exception e) {
547            throw (IOException) new IOException(
548                "unable to construct LogFile instance").initCause(e);
549        }
550
551        if (truncate) {
552            initializeLogFile();
553        }
554    }
555
556    /**
557     * Creates a new log file, truncated and initialized with the format
558     * version number preferred by this implementation.
559     * <p>Environment: inited, synchronized
560     * <p>Precondition: valid: log, log contains nothing useful
561     * <p>Postcondition: if successful, log is initialised with the format
562     * version number (Preferred{Major,Minor}Version), and logBytes is
563     * set to the resulting size of the updatelog, and logEntries is set to
564     * zero.  Otherwise, log is in an indeterminate state, and logBytes
565     * is unchanged, and logEntries is unchanged.
566     *
567     * @exception IOException If an I/O error has occurred.
568     */
569    private void initializeLogFile()
570        throws IOException
571    {
572        log.setLength(0);
573        majorFormatVersion = PreferredMajorVersion;
574        writeInt(log, PreferredMajorVersion);
575        minorFormatVersion = PreferredMinorVersion;
576        writeInt(log, PreferredMinorVersion);
577        logBytes = intBytes * 2;
578        logEntries = 0;
579    }
580
581
582    /**
583     * Writes out version number to file.
584     *
585     * @param newVersion if true, writes to a new version file
586     * @exception IOException If an I/O error has occurred.
587     */
588    private void writeVersionFile(boolean newVersion) throws IOException {
589        String name;
590        if (newVersion) {
591            name = newVersionFile;
592        } else {
593            name = versionFile;
594        }
595        try (FileOutputStream fos = new FileOutputStream(fName(name));
596             DataOutputStream out = new DataOutputStream(fos)) {
597            writeInt(out, version);
598        }
599    }
600
601    /**
602     * Creates the initial version file
603     *
604     * @exception IOException If an I/O error has occurred.
605     */
606    private void createFirstVersion() throws IOException {
607        version = 0;
608        writeVersionFile(false);
609    }
610
611    /**
612     * Commits (atomically) the new version.
613     *
614     * @exception IOException If an I/O error has occurred.
615     */
616    private void commitToNewVersion() throws IOException {
617        writeVersionFile(false);
618        deleteNewVersionFile();
619    }
620
621    /**
622     * Reads version number from a file.
623     *
624     * @param name the name of the version file
625     * @return the version
626     * @exception IOException If an I/O error has occurred.
627     */
628    private int readVersion(String name) throws IOException {
629        try (DataInputStream in = new DataInputStream
630                (new FileInputStream(name))) {
631            return in.readInt();
632        }
633    }
634
635    /**
636     * Sets the version.  If version file does not exist, the initial
637     * version file is created.
638     *
639     * @exception IOException If an I/O error has occurred.
640     */
641    private void getVersion() throws IOException {
642        try {
643            version = readVersion(fName(newVersionFile));
644            commitToNewVersion();
645        } catch (IOException e) {
646            try {
647                deleteNewVersionFile();
648            }
649            catch (IOException ex) {
650            }
651
652            try {
653                version = readVersion(fName(versionFile));
654            }
655            catch (IOException ex) {
656                createFirstVersion();
657            }
658        }
659    }
660
661    /**
662     * Applies outstanding updates to the snapshot.
663     *
664     * @param state the most recent snapshot
665     * @exception IOException If serious log corruption is detected or
666     * if an exception occurred during a readUpdate callback or if
667     * other I/O error has occurred.
668     * @return the resulting state of the object after all updates
669     */
670    private Object recoverUpdates(Object state)
671        throws IOException
672    {
673        logBytes = 0;
674        logEntries = 0;
675
676        if (version == 0) return state;
677
678        String fname = versionName(logfilePrefix);
679        InputStream in =
680                new BufferedInputStream(new FileInputStream(fname));
681        DataInputStream dataIn = new DataInputStream(in);
682
683        if (Debug)
684            System.err.println("log.debug: reading updates from " + fname);
685
686        try {
687            majorFormatVersion = dataIn.readInt(); logBytes += intBytes;
688            minorFormatVersion = dataIn.readInt(); logBytes += intBytes;
689        } catch (EOFException e) {
690            /* This is a log which was corrupted and/or cleared (by
691             * fsck or equivalent).  This is not an error.
692             */
693            openLogFile(true);  // create and truncate
694            in = null;
695        }
696        /* A new major version number is a catastrophe (it means
697         * that the file format is incompatible with older
698         * clients, and we'll only be breaking things by trying to
699         * use the log).  A new minor version is no big deal for
700         * upward compatibility.
701         */
702        if (majorFormatVersion != PreferredMajorVersion) {
703            if (Debug) {
704                System.err.println("log.debug: major version mismatch: " +
705                        majorFormatVersion + "." + minorFormatVersion);
706            }
707            throw new IOException("Log file " + logName + " has a " +
708                                  "version " + majorFormatVersion +
709                                  "." + minorFormatVersion +
710                                  " format, and this implementation " +
711                                  " understands only version " +
712                                  PreferredMajorVersion + "." +
713                                  PreferredMinorVersion);
714        }
715
716        try {
717            while (in != null) {
718                int updateLen = 0;
719
720                try {
721                    updateLen = dataIn.readInt();
722                } catch (EOFException e) {
723                    if (Debug)
724                        System.err.println("log.debug: log was sync'd cleanly");
725                    break;
726                }
727                if (updateLen <= 0) {/* crashed while writing last log entry */
728                    if (Debug) {
729                        System.err.println(
730                            "log.debug: last update incomplete, " +
731                            "updateLen = 0x" +
732                            Integer.toHexString(updateLen));
733                    }
734                    break;
735                }
736
737                // this is a fragile use of available() which relies on the
738                // twin facts that BufferedInputStream correctly consults
739                // the underlying stream, and that FileInputStream returns
740                // the number of bytes remaining in the file (via FIONREAD).
741                if (in.available() < updateLen) {
742                    /* corrupted record at end of log (can happen since we
743                     * do only one fsync)
744                     */
745                    if (Debug)
746                        System.err.println("log.debug: log was truncated");
747                    break;
748                }
749
750                if (Debug)
751                    System.err.println("log.debug: rdUpdate size " + updateLen);
752                try {
753                    state = handler.readUpdate(new LogInputStream(in, updateLen),
754                                          state);
755                } catch (IOException e) {
756                    throw e;
757                } catch (Exception e) {
758                    e.printStackTrace();
759                    throw new IOException("read update failed with " +
760                                          "exception: " + e);
761                }
762                logBytes += (intBytes + updateLen);
763                logEntries++;
764            } /* while */
765        } finally {
766            if (in != null)
767                in.close();
768        }
769
770        if (Debug)
771            System.err.println("log.debug: recovered updates: " + logEntries);
772
773        /* reopen log file at end */
774        openLogFile(false);
775
776        // avoid accessing a null log field
777        if (log == null) {
778            throw new IOException("rmid's log is inaccessible, " +
779                "it may have been corrupted or closed");
780        }
781
782        log.seek(logBytes);
783        log.setLength(logBytes);
784
785        return state;
786    }
787
788    /**
789     * ReliableLog's log file implementation.  This implementation
790     * is subclassable for testing purposes.
791     */
792    public static class LogFile extends RandomAccessFile {
793
794        private final FileDescriptor fd;
795
796        /**
797         * Constructs a LogFile and initializes the file descriptor.
798         **/
799        public LogFile(String name, String mode)
800            throws FileNotFoundException, IOException
801        {
802            super(name, mode);
803            this.fd = getFD();
804        }
805
806        /**
807         * Invokes sync on the file descriptor for this log file.
808         */
809        protected void sync() throws IOException {
810            fd.sync();
811        }
812
813        /**
814         * Returns true if writing 4 bytes starting at the specified file
815         * position, would span a 512 byte sector boundary; otherwise returns
816         * false.
817         **/
818        protected boolean checkSpansBoundary(long fp) {
819            return  fp % 512 > 508;
820        }
821    }
822}
823