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