1 /* 2 * Copyright (c) 1997, 2008, 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 26 package sun.rmi.log; 27 28 import java.io.*; 29 import java.lang.reflect.Constructor; 30 import java.rmi.server.RMIClassLoader; 31 import java.security.AccessController; 32 import java.security.PrivilegedAction; 33 import sun.security.action.GetBooleanAction; 34 import 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 */ 81 public 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 = 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 = 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", e); 384 } 385 lastSnapshot = System.currentTimeMillis(); 386 } finally { 387 out.close(); 388 snapshotBytes = snapshotFile.length(); 389 } 390 391 openLogFile(true); 392 writeVersionFile(true); 393 commitToNewVersion(); 394 deleteSnapshot(oldVersion); 395 deleteLogFile(oldVersion); 396 } 397 398 /** 399 * Close the stable storage directory in an orderly manner. 400 * 401 * @exception IOException If an I/O error occurs when the log is 402 * closed 403 */ 404 public synchronized void close() throws IOException { 405 if (log == null) return; 406 try { 407 log.close(); 408 } finally { 409 log = null; 410 } 411 } 412 413 /** 414 * Returns the size of the snapshot file in bytes; 415 */ 416 public long snapshotSize() { 417 return snapshotBytes; 418 } 419 420 /** 421 * Returns the size of the log file in bytes; 422 */ 423 public long logSize() { 424 return logBytes; 425 } 426 427 /* private methods */ 428 429 /** 430 * Write an int value in single write operation. This method 431 * assumes that the caller is synchronized on the log file. 432 * 433 * @param out output stream 434 * @param val int value 435 * @throws IOException if any other I/O error occurs 436 */ 437 private void writeInt(DataOutput out, int val) 438 throws IOException 439 { 440 intBuf[0] = (byte) (val >> 24); 441 intBuf[1] = (byte) (val >> 16); 442 intBuf[2] = (byte) (val >> 8); 443 intBuf[3] = (byte) val; 444 out.write(intBuf); 445 } 446 447 /** 448 * Generates a filename prepended with the stable storage directory path. 449 * 450 * @param name the leaf name of the file 451 */ 452 private String fName(String name) { 453 return dir.getPath() + File.separator + name; 454 } 455 456 /** 457 * Generates a version 0 filename prepended with the stable storage 458 * directory path 459 * 460 * @param name version file name 461 */ 462 private String versionName(String name) { 463 return versionName(name, 0); 464 } 465 466 /** 467 * Generates a version filename prepended with the stable storage 468 * directory path with the version number as a suffix. 469 * 470 * @param name version file name 471 * @thisversion a version number 472 */ 473 private String versionName(String prefix, int ver) { 474 ver = (ver == 0) ? version : ver; 475 return fName(prefix) + String.valueOf(ver); 476 } 477 478 /** 479 * Increments the directory version number. 480 */ 481 private void incrVersion() { 482 do { version++; } while (version==0); 483 } 484 485 /** 486 * Delete a file. 487 * 488 * @param name the name of the file 489 * @exception IOException If new version file couldn't be removed 490 */ 491 private void deleteFile(String name) throws IOException { 492 493 File f = new File(name); 494 if (!f.delete()) 495 throw new IOException("couldn't remove file: " + name); 496 } 497 498 /** 499 * Removes the new version number file. 500 * 501 * @exception IOException If an I/O error has occurred. 502 */ 503 private void deleteNewVersionFile() throws IOException { 504 deleteFile(fName(newVersionFile)); 505 } 506 507 /** 508 * Removes the snapshot file. 509 * 510 * @param ver the version to remove 511 * @exception IOException If an I/O error has occurred. 512 */ 513 private void deleteSnapshot(int ver) throws IOException { 514 if (ver == 0) return; 515 deleteFile(versionName(snapshotPrefix, ver)); 516 } 517 518 /** 519 * Removes the log file. 520 * 521 * @param ver the version to remove 522 * @exception IOException If an I/O error has occurred. 523 */ 524 private void deleteLogFile(int ver) throws IOException { 525 if (ver == 0) return; 526 deleteFile(versionName(logfilePrefix, ver)); 527 } 528 529 /** 530 * Opens the log file in read/write mode. If file does not exist, it is 531 * created. 532 * 533 * @param truncate if true and file exists, file is truncated to zero 534 * length 535 * @exception IOException If an I/O error has occurred. 536 */ 537 private void openLogFile(boolean truncate) throws IOException { 538 try { 539 close(); 540 } catch (IOException e) { /* assume this is okay */ 541 } 542 543 logName = versionName(logfilePrefix); 544 545 try { 546 log = (logClassConstructor == null ? 547 new LogFile(logName, "rw") : 548 logClassConstructor.newInstance(logName, "rw")); 549 } catch (Exception e) { 550 throw (IOException) new IOException( 551 "unable to construct LogFile instance").initCause(e); 552 } 553 554 if (truncate) { 555 initializeLogFile(); 556 } 557 } 558 559 /** 560 * Creates a new log file, truncated and initialized with the format 561 * version number preferred by this implementation. 562 * <p>Environment: inited, synchronized 563 * <p>Precondition: valid: log, log contains nothing useful 564 * <p>Postcondition: if successful, log is initialised with the format 565 * version number (Preferred{Major,Minor}Version), and logBytes is 566 * set to the resulting size of the updatelog, and logEntries is set to 567 * zero. Otherwise, log is in an indeterminate state, and logBytes 568 * is unchanged, and logEntries is unchanged. 569 * 570 * @exception IOException If an I/O error has occurred. 571 */ 572 private void initializeLogFile() 573 throws IOException 574 { 575 log.setLength(0); 576 majorFormatVersion = PreferredMajorVersion; 577 writeInt(log, PreferredMajorVersion); 578 minorFormatVersion = PreferredMinorVersion; 579 writeInt(log, PreferredMinorVersion); 580 logBytes = intBytes * 2; 581 logEntries = 0; 582 } 583 584 585 /** 586 * Writes out version number to file. 587 * 588 * @param newVersion if true, writes to a new version file 589 * @exception IOException If an I/O error has occurred. 590 */ 591 private void writeVersionFile(boolean newVersion) throws IOException { 592 String name; 593 if (newVersion) { 594 name = newVersionFile; 595 } else { 596 name = versionFile; 597 } 598 DataOutputStream out = 599 new DataOutputStream(new FileOutputStream(fName(name))); 600 writeInt(out, version); 601 out.close(); 602 } 603 604 /** 605 * Creates the initial version file 606 * 607 * @exception IOException If an I/O error has occurred. 608 */ 609 private void createFirstVersion() throws IOException { 610 version = 0; 611 writeVersionFile(false); 612 } 613 614 /** 615 * Commits (atomically) the new version. 616 * 617 * @exception IOException If an I/O error has occurred. 618 */ 619 private void commitToNewVersion() throws IOException { 620 writeVersionFile(false); 621 deleteNewVersionFile(); 622 } 623 624 /** 625 * Reads version number from a file. 626 * 627 * @param name the name of the version file 628 * @return the version 629 * @exception IOException If an I/O error has occurred. 630 */ 631 private int readVersion(String name) throws IOException { 632 DataInputStream in = new DataInputStream(new FileInputStream(name)); 633 try { 634 return in.readInt(); 635 } finally { 636 in.close(); 637 } 638 } 639 640 /** 641 * Sets the version. If version file does not exist, the initial 642 * version file is created. 643 * 644 * @exception IOException If an I/O error has occurred. 645 */ 646 private void getVersion() throws IOException { 647 try { 648 version = readVersion(fName(newVersionFile)); 649 commitToNewVersion(); 650 } catch (IOException e) { 651 try { 652 deleteNewVersionFile(); 653 } 654 catch (IOException ex) { 655 } 656 657 try { 658 version = readVersion(fName(versionFile)); 659 } 660 catch (IOException ex) { 661 createFirstVersion(); 662 } 663 } 664 } 665 666 /** 667 * Applies outstanding updates to the snapshot. 668 * 669 * @param state the most recent snapshot 670 * @exception IOException If serious log corruption is detected or 671 * if an exception occurred during a readUpdate callback or if 672 * other I/O error has occurred. 673 * @return the resulting state of the object after all updates 674 */ 675 private Object recoverUpdates(Object state) 676 throws IOException 677 { 678 logBytes = 0; 679 logEntries = 0; 680 681 if (version == 0) return state; 682 683 String fname = versionName(logfilePrefix); 684 InputStream in = 685 new BufferedInputStream(new FileInputStream(fname)); 686 DataInputStream dataIn = new DataInputStream(in); 687 688 if (Debug) 689 System.err.println("log.debug: reading updates from " + fname); 690 691 try { 692 majorFormatVersion = dataIn.readInt(); logBytes += intBytes; 693 minorFormatVersion = dataIn.readInt(); logBytes += intBytes; 694 } catch (EOFException e) { 695 /* This is a log which was corrupted and/or cleared (by 696 * fsck or equivalent). This is not an error. 697 */ 698 openLogFile(true); // create and truncate 699 in = null; 700 } 701 /* A new major version number is a catastrophe (it means 702 * that the file format is incompatible with older 703 * clients, and we'll only be breaking things by trying to 704 * use the log). A new minor version is no big deal for 705 * upward compatibility. 706 */ 707 if (majorFormatVersion != PreferredMajorVersion) { 708 if (Debug) { 709 System.err.println("log.debug: major version mismatch: " + 710 majorFormatVersion + "." + minorFormatVersion); 711 } 712 throw new IOException("Log file " + logName + " has a " + 713 "version " + majorFormatVersion + 714 "." + minorFormatVersion + 715 " format, and this implementation " + 716 " understands only version " + 717 PreferredMajorVersion + "." + 718 PreferredMinorVersion); 719 } 720 721 try { 722 while (in != null) { 723 int updateLen = 0; 724 725 try { 726 updateLen = dataIn.readInt(); 727 } catch (EOFException e) { 728 if (Debug) 729 System.err.println("log.debug: log was sync'd cleanly"); 730 break; 731 } 732 if (updateLen <= 0) {/* crashed while writing last log entry */ 733 if (Debug) { 734 System.err.println( 735 "log.debug: last update incomplete, " + 736 "updateLen = 0x" + 737 Integer.toHexString(updateLen)); 738 } 739 break; 740 } 741 742 // this is a fragile use of available() which relies on the 743 // twin facts that BufferedInputStream correctly consults 744 // the underlying stream, and that FileInputStream returns 745 // the number of bytes remaining in the file (via FIONREAD). 746 if (in.available() < updateLen) { 747 /* corrupted record at end of log (can happen since we 748 * do only one fsync) 749 */ 750 if (Debug) 751 System.err.println("log.debug: log was truncated"); 752 break; 753 } 754 755 if (Debug) 756 System.err.println("log.debug: rdUpdate size " + updateLen); 757 try { 758 state = handler.readUpdate(new LogInputStream(in, updateLen), 759 state); 760 } catch (IOException e) { 761 throw e; 762 } catch (Exception e) { 763 e.printStackTrace(); 764 throw new IOException("read update failed with " + 765 "exception: " + e); 766 } 767 logBytes += (intBytes + updateLen); 768 logEntries++; 769 } /* while */ 770 } finally { 771 if (in != null) 772 in.close(); 773 } 774 775 if (Debug) 776 System.err.println("log.debug: recovered updates: " + logEntries); 777 778 /* reopen log file at end */ 779 openLogFile(false); 780 781 // avoid accessing a null log field 782 if (log == null) { 783 throw new IOException("rmid's log is inaccessible, " + 784 "it may have been corrupted or closed"); 785 } 786 787 log.seek(logBytes); 788 log.setLength(logBytes); 789 790 return state; 791 } 792 793 /** 794 * ReliableLog's log file implementation. This implementation 795 * is subclassable for testing purposes. 796 */ 797 public static class LogFile extends RandomAccessFile { 798 799 private final FileDescriptor fd; 800 801 /** 802 * Constructs a LogFile and initializes the file descriptor. 803 **/ 804 public LogFile(String name, String mode) 805 throws FileNotFoundException, IOException 806 { 807 super(name, mode); 808 this.fd = getFD(); 809 } 810 811 /** 812 * Invokes sync on the file descriptor for this log file. 813 */ 814 protected void sync() throws IOException { 815 fd.sync(); 816 } 817 818 /** 819 * Returns true if writing 4 bytes starting at the specified file 820 * position, would span a 512 byte sector boundary; otherwise returns 821 * false. 822 **/ 823 protected boolean checkSpansBoundary(long fp) { 824 return fp % 512 > 508; 825 } 826 } 827 }