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 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 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 */ 79 public 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 }