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