001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015-2016 ForgeRock AS. 015 */ 016package org.opends.server.backends.jeb; 017 018import static com.sleepycat.je.EnvironmentConfig.*; 019import static com.sleepycat.je.LockMode.READ_COMMITTED; 020import static com.sleepycat.je.LockMode.RMW; 021import static com.sleepycat.je.OperationStatus.*; 022 023import static org.forgerock.util.Utils.*; 024import static org.opends.messages.BackendMessages.*; 025import static org.opends.messages.UtilityMessages.*; 026import static org.opends.server.backends.pluggable.spi.StorageUtils.*; 027import static org.opends.server.util.StaticUtils.*; 028 029import java.io.File; 030import java.io.FileFilter; 031import java.io.IOException; 032import java.nio.file.Files; 033import java.nio.file.Path; 034import java.util.ArrayList; 035import java.util.Collections; 036import java.util.HashMap; 037import java.util.HashSet; 038import java.util.List; 039import java.util.ListIterator; 040import java.util.Map; 041import java.util.NoSuchElementException; 042import java.util.Objects; 043import java.util.Set; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.ConcurrentMap; 046import java.util.concurrent.TimeUnit; 047 048import org.forgerock.i18n.LocalizableMessage; 049import org.forgerock.i18n.slf4j.LocalizedLogger; 050import org.forgerock.opendj.config.server.ConfigChangeResult; 051import org.forgerock.opendj.config.server.ConfigException; 052import org.forgerock.opendj.ldap.ByteSequence; 053import org.forgerock.opendj.ldap.ByteString; 054import org.forgerock.util.Reject; 055import org.opends.server.admin.server.ConfigurationChangeListener; 056import org.opends.server.admin.std.server.JEBackendCfg; 057import org.opends.server.api.Backupable; 058import org.opends.server.api.DiskSpaceMonitorHandler; 059import org.opends.server.backends.pluggable.spi.AccessMode; 060import org.opends.server.backends.pluggable.spi.Cursor; 061import org.opends.server.backends.pluggable.spi.Importer; 062import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException; 063import org.opends.server.backends.pluggable.spi.ReadOperation; 064import org.opends.server.backends.pluggable.spi.SequentialCursor; 065import org.opends.server.backends.pluggable.spi.Storage; 066import org.opends.server.backends.pluggable.spi.StorageRuntimeException; 067import org.opends.server.backends.pluggable.spi.StorageStatus; 068import org.opends.server.backends.pluggable.spi.StorageUtils; 069import org.opends.server.backends.pluggable.spi.TreeName; 070import org.opends.server.backends.pluggable.spi.UpdateFunction; 071import org.opends.server.backends.pluggable.spi.WriteOperation; 072import org.opends.server.backends.pluggable.spi.WriteableTransaction; 073import org.opends.server.core.DirectoryServer; 074import org.opends.server.core.MemoryQuota; 075import org.opends.server.core.ServerContext; 076import org.opends.server.extensions.DiskSpaceMonitor; 077import org.opends.server.types.BackupConfig; 078import org.opends.server.types.BackupDirectory; 079import org.opends.server.types.DirectoryException; 080import org.opends.server.types.RestoreConfig; 081import org.opends.server.util.BackupManager; 082 083import com.sleepycat.je.CursorConfig; 084import com.sleepycat.je.Database; 085import com.sleepycat.je.DatabaseConfig; 086import com.sleepycat.je.DatabaseEntry; 087import com.sleepycat.je.DatabaseException; 088import com.sleepycat.je.DatabaseNotFoundException; 089import com.sleepycat.je.Durability; 090import com.sleepycat.je.Environment; 091import com.sleepycat.je.EnvironmentConfig; 092import com.sleepycat.je.OperationStatus; 093import com.sleepycat.je.Transaction; 094import com.sleepycat.je.TransactionConfig; 095 096/** Berkeley DB Java Edition (JE for short) database implementation of the {@link Storage} engine. */ 097public final class JEStorage implements Storage, Backupable, ConfigurationChangeListener<JEBackendCfg>, 098 DiskSpaceMonitorHandler 099{ 100 /** JE implementation of the {@link Cursor} interface. */ 101 private static final class CursorImpl implements Cursor<ByteString, ByteString> 102 { 103 private ByteString currentKey; 104 private ByteString currentValue; 105 private boolean isDefined; 106 private final com.sleepycat.je.Cursor cursor; 107 private final DatabaseEntry dbKey = new DatabaseEntry(); 108 private final DatabaseEntry dbValue = new DatabaseEntry(); 109 110 private CursorImpl(com.sleepycat.je.Cursor cursor) 111 { 112 this.cursor = cursor; 113 } 114 115 @Override 116 public void close() 117 { 118 closeSilently(cursor); 119 } 120 121 @Override 122 public boolean isDefined() 123 { 124 return isDefined; 125 } 126 127 @Override 128 public ByteString getKey() 129 { 130 if (currentKey == null) 131 { 132 throwIfNotSuccess(); 133 currentKey = ByteString.wrap(dbKey.getData()); 134 } 135 return currentKey; 136 } 137 138 @Override 139 public ByteString getValue() 140 { 141 if (currentValue == null) 142 { 143 throwIfNotSuccess(); 144 currentValue = ByteString.wrap(dbValue.getData()); 145 } 146 return currentValue; 147 } 148 149 @Override 150 public boolean next() 151 { 152 clearCurrentKeyAndValue(); 153 try 154 { 155 isDefined = cursor.getNext(dbKey, dbValue, null) == SUCCESS; 156 return isDefined; 157 } 158 catch (DatabaseException e) 159 { 160 throw new StorageRuntimeException(e); 161 } 162 } 163 164 @Override 165 public void delete() throws NoSuchElementException, UnsupportedOperationException 166 { 167 throwIfNotSuccess(); 168 try 169 { 170 cursor.delete(); 171 } 172 catch (DatabaseException e) 173 { 174 throw new StorageRuntimeException(e); 175 } 176 } 177 178 @Override 179 public boolean positionToKey(final ByteSequence key) 180 { 181 clearCurrentKeyAndValue(); 182 setData(dbKey, key); 183 try 184 { 185 isDefined = cursor.getSearchKey(dbKey, dbValue, null) == SUCCESS; 186 return isDefined; 187 } 188 catch (DatabaseException e) 189 { 190 throw new StorageRuntimeException(e); 191 } 192 } 193 194 @Override 195 public boolean positionToKeyOrNext(final ByteSequence key) 196 { 197 clearCurrentKeyAndValue(); 198 setData(dbKey, key); 199 try 200 { 201 isDefined = cursor.getSearchKeyRange(dbKey, dbValue, null) == SUCCESS; 202 return isDefined; 203 } 204 catch (DatabaseException e) 205 { 206 throw new StorageRuntimeException(e); 207 } 208 } 209 210 @Override 211 public boolean positionToIndex(int index) 212 { 213 clearCurrentKeyAndValue(); 214 try 215 { 216 isDefined = cursor.getFirst(dbKey, dbValue, null) == SUCCESS; 217 if (!isDefined) 218 { 219 return false; 220 } 221 else if (index == 0) 222 { 223 return true; 224 } 225 226 // equivalent to READ_UNCOMMITTED 227 long skipped = cursor.skipNext(index, dbKey, dbValue, null); 228 if (skipped == index) 229 { 230 isDefined = cursor.getCurrent(dbKey, dbValue, null) == SUCCESS; 231 } 232 else 233 { 234 isDefined = false; 235 } 236 return isDefined; 237 } 238 catch (DatabaseException e) 239 { 240 throw new StorageRuntimeException(e); 241 } 242 } 243 244 @Override 245 public boolean positionToLastKey() 246 { 247 clearCurrentKeyAndValue(); 248 try 249 { 250 isDefined = cursor.getLast(dbKey, dbValue, null) == SUCCESS; 251 return isDefined; 252 } 253 catch (DatabaseException e) 254 { 255 throw new StorageRuntimeException(e); 256 } 257 } 258 259 private void clearCurrentKeyAndValue() 260 { 261 currentKey = null; 262 currentValue = null; 263 } 264 265 private void throwIfNotSuccess() 266 { 267 if (!isDefined()) 268 { 269 throw new NoSuchElementException(); 270 } 271 } 272 } 273 274 /** JE implementation of the {@link Importer} interface. */ 275 private final class ImporterImpl implements Importer 276 { 277 private final Map<TreeName, Database> trees = new HashMap<>(); 278 279 private Database getOrOpenTree(TreeName treeName) 280 { 281 return getOrOpenTree0(trees, treeName); 282 } 283 284 @Override 285 public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value) 286 { 287 try 288 { 289 getOrOpenTree(treeName).put(null, db(key), db(value)); 290 } 291 catch (DatabaseException e) 292 { 293 throw new StorageRuntimeException(e); 294 } 295 } 296 297 @Override 298 public ByteString read(final TreeName treeName, final ByteSequence key) 299 { 300 try 301 { 302 DatabaseEntry dbValue = new DatabaseEntry(); 303 boolean isDefined = getOrOpenTree(treeName).get(null, db(key), dbValue, null) == SUCCESS; 304 return valueToBytes(dbValue, isDefined); 305 } 306 catch (DatabaseException e) 307 { 308 throw new StorageRuntimeException(e); 309 } 310 } 311 312 @Override 313 public SequentialCursor<ByteString, ByteString> openCursor(TreeName treeName) 314 { 315 try 316 { 317 return new CursorImpl(getOrOpenTree(treeName).openCursor(null, new CursorConfig())); 318 } 319 catch (DatabaseException e) 320 { 321 throw new StorageRuntimeException(e); 322 } 323 } 324 325 @Override 326 public void clearTree(TreeName treeName) 327 { 328 env.truncateDatabase(null, toDatabaseName(treeName), false); 329 } 330 331 @Override 332 public void close() 333 { 334 closeSilently(trees.values()); 335 trees.clear(); 336 JEStorage.this.close(); 337 } 338 } 339 340 /** JE implementation of the {@link WriteableTransaction} interface. */ 341 private final class WriteableTransactionImpl implements WriteableTransaction 342 { 343 private final Transaction txn; 344 345 private WriteableTransactionImpl(Transaction txn) 346 { 347 this.txn = txn; 348 } 349 350 /** 351 * This is currently needed for import-ldif: 352 * <ol> 353 * <li>Opening the EntryContainer calls {@link #openTree(TreeName, boolean)} for each index</li> 354 * <li>Then the underlying storage is closed</li> 355 * <li>Then {@link Importer#startImport()} is called</li> 356 * <li>Then ID2Entry#put() is called</li> 357 * <li>Which in turn calls ID2Entry#encodeEntry()</li> 358 * <li>Which in turn finally calls PersistentCompressedSchema#store()</li> 359 * <li>Which uses a reference to the storage (that was closed before calling startImport()) and 360 * uses it as if it was open</li> 361 * </ol> 362 */ 363 private Database getOrOpenTree(TreeName treeName) 364 { 365 try 366 { 367 return getOrOpenTree0(trees, treeName); 368 } 369 catch (Exception e) 370 { 371 throw new StorageRuntimeException(e); 372 } 373 } 374 375 @Override 376 public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value) 377 { 378 try 379 { 380 final OperationStatus status = getOrOpenTree(treeName).put(txn, db(key), db(value)); 381 if (status != SUCCESS) 382 { 383 throw new StorageRuntimeException(putErrorMsg(treeName, key, value, "did not succeed: " + status)); 384 } 385 } 386 catch (DatabaseException e) 387 { 388 throw new StorageRuntimeException(putErrorMsg(treeName, key, value, "threw an exception"), e); 389 } 390 } 391 392 private String putErrorMsg(TreeName treeName, ByteSequence key, ByteSequence value, String msg) 393 { 394 return "put(treeName=" + treeName + ", key=" + key + ", value=" + value + ") " + msg; 395 } 396 397 @Override 398 public boolean delete(final TreeName treeName, final ByteSequence key) 399 { 400 try 401 { 402 return getOrOpenTree(treeName).delete(txn, db(key)) == SUCCESS; 403 } 404 catch (DatabaseException e) 405 { 406 throw new StorageRuntimeException(deleteErrorMsg(treeName, key, "threw an exception"), e); 407 } 408 } 409 410 private String deleteErrorMsg(TreeName treeName, ByteSequence key, String msg) 411 { 412 return "delete(treeName=" + treeName + ", key=" + key + ") " + msg; 413 } 414 415 @Override 416 public long getRecordCount(TreeName treeName) 417 { 418 try 419 { 420 return getOrOpenTree(treeName).count(); 421 } 422 catch (DatabaseException e) 423 { 424 throw new StorageRuntimeException(e); 425 } 426 } 427 428 @Override 429 public Cursor<ByteString, ByteString> openCursor(final TreeName treeName) 430 { 431 try 432 { 433 return new CursorImpl(getOrOpenTree(treeName).openCursor(txn, CursorConfig.READ_COMMITTED)); 434 } 435 catch (DatabaseException e) 436 { 437 throw new StorageRuntimeException(e); 438 } 439 } 440 441 @Override 442 public ByteString read(final TreeName treeName, final ByteSequence key) 443 { 444 try 445 { 446 DatabaseEntry dbValue = new DatabaseEntry(); 447 boolean isDefined = getOrOpenTree(treeName).get(txn, db(key), dbValue, READ_COMMITTED) == SUCCESS; 448 return valueToBytes(dbValue, isDefined); 449 } 450 catch (DatabaseException e) 451 { 452 throw new StorageRuntimeException(e); 453 } 454 } 455 456 @Override 457 public boolean update(final TreeName treeName, final ByteSequence key, final UpdateFunction f) 458 { 459 try 460 { 461 final Database tree = getOrOpenTree(treeName); 462 final DatabaseEntry dbKey = db(key); 463 final DatabaseEntry dbValue = new DatabaseEntry(); 464 for (;;) 465 { 466 final boolean isDefined = tree.get(txn, dbKey, dbValue, RMW) == SUCCESS; 467 final ByteSequence oldValue = valueToBytes(dbValue, isDefined); 468 final ByteSequence newValue = f.computeNewValue(oldValue); 469 if (Objects.equals(newValue, oldValue)) 470 { 471 return false; 472 } 473 if (newValue == null) 474 { 475 return tree.delete(txn, dbKey) == SUCCESS; 476 } 477 setData(dbValue, newValue); 478 if (isDefined) 479 { 480 return tree.put(txn, dbKey, dbValue) == SUCCESS; 481 } 482 else if (tree.putNoOverwrite(txn, dbKey, dbValue) == SUCCESS) 483 { 484 return true; 485 } 486 // else retry due to phantom read: another thread inserted a record 487 } 488 } 489 catch (DatabaseException e) 490 { 491 throw new StorageRuntimeException(e); 492 } 493 } 494 495 @Override 496 public void openTree(final TreeName treeName, boolean createOnDemand) 497 { 498 getOrOpenTree(treeName); 499 } 500 501 @Override 502 public void deleteTree(final TreeName treeName) 503 { 504 try 505 { 506 synchronized (trees) 507 { 508 closeSilently(trees.remove(treeName)); 509 env.removeDatabase(txn, toDatabaseName(treeName)); 510 } 511 } 512 catch (DatabaseNotFoundException e) 513 { 514 // This is fine: end result is what we wanted 515 } 516 catch (DatabaseException e) 517 { 518 throw new StorageRuntimeException(e); 519 } 520 } 521 } 522 523 /** JE read-only implementation of {@link StorageImpl} interface. */ 524 private final class ReadOnlyTransactionImpl implements WriteableTransaction 525 { 526 private final WriteableTransactionImpl delegate; 527 528 ReadOnlyTransactionImpl(WriteableTransactionImpl delegate) 529 { 530 this.delegate = delegate; 531 } 532 533 @Override 534 public ByteString read(TreeName treeName, ByteSequence key) 535 { 536 return delegate.read(treeName, key); 537 } 538 539 @Override 540 public Cursor<ByteString, ByteString> openCursor(TreeName treeName) 541 { 542 return delegate.openCursor(treeName); 543 } 544 545 @Override 546 public long getRecordCount(TreeName treeName) 547 { 548 return delegate.getRecordCount(treeName); 549 } 550 551 @Override 552 public void openTree(TreeName treeName, boolean createOnDemand) 553 { 554 if (createOnDemand) 555 { 556 throw new ReadOnlyStorageException(); 557 } 558 delegate.openTree(treeName, false); 559 } 560 561 @Override 562 public void deleteTree(TreeName name) 563 { 564 throw new ReadOnlyStorageException(); 565 } 566 567 @Override 568 public void put(TreeName treeName, ByteSequence key, ByteSequence value) 569 { 570 throw new ReadOnlyStorageException(); 571 } 572 573 @Override 574 public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f) 575 { 576 throw new ReadOnlyStorageException(); 577 } 578 579 @Override 580 public boolean delete(TreeName treeName, ByteSequence key) 581 { 582 throw new ReadOnlyStorageException(); 583 } 584 } 585 586 private WriteableTransaction newWriteableTransaction(Transaction txn) 587 { 588 final WriteableTransactionImpl writeableStorage = new WriteableTransactionImpl(txn); 589 return accessMode.isWriteable() ? writeableStorage : new ReadOnlyTransactionImpl(writeableStorage); 590 } 591 592 private static final int IMPORT_DB_CACHE_SIZE = 32 * MB; 593 594 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 595 596 /** Use read committed isolation instead of the default which is repeatable read. */ 597 private static final TransactionConfig TXN_READ_COMMITTED = new TransactionConfig().setReadCommitted(true); 598 599 private final ServerContext serverContext; 600 private final File backendDirectory; 601 private JEBackendCfg config; 602 private AccessMode accessMode; 603 604 private Environment env; 605 private EnvironmentConfig envConfig; 606 private MemoryQuota memQuota; 607 private JEMonitor monitor; 608 private DiskSpaceMonitor diskMonitor; 609 private StorageStatus storageStatus = StorageStatus.working(); 610 private final ConcurrentMap<TreeName, Database> trees = new ConcurrentHashMap<>(); 611 612 /** 613 * Creates a new JE storage with the provided configuration. 614 * 615 * @param cfg 616 * The configuration. 617 * @param serverContext 618 * This server instance context 619 * @throws ConfigException 620 * if memory cannot be reserved 621 */ 622 JEStorage(final JEBackendCfg cfg, ServerContext serverContext) throws ConfigException 623 { 624 this.serverContext = serverContext; 625 backendDirectory = getBackendDirectory(cfg); 626 config = cfg; 627 cfg.addJEChangeListener(this); 628 } 629 630 private Database getOrOpenTree0(Map<TreeName, Database> trees, TreeName treeName) 631 { 632 Database tree = trees.get(treeName); 633 if (tree == null) 634 { 635 synchronized (trees) 636 { 637 tree = trees.get(treeName); 638 if (tree == null) 639 { 640 tree = env.openDatabase(null, toDatabaseName(treeName), dbConfig()); 641 trees.put(treeName, tree); 642 } 643 } 644 } 645 return tree; 646 } 647 648 private void buildConfiguration(AccessMode accessMode, boolean isImport) throws ConfigException 649 { 650 this.accessMode = accessMode; 651 652 if (isImport) 653 { 654 envConfig = new EnvironmentConfig(); 655 envConfig 656 .setTransactional(false) 657 .setAllowCreate(true) 658 .setLockTimeout(0, TimeUnit.SECONDS) 659 .setTxnTimeout(0, TimeUnit.SECONDS) 660 .setCacheSize(IMPORT_DB_CACHE_SIZE) 661 .setDurability(Durability.COMMIT_NO_SYNC) 662 .setConfigParam(CLEANER_MIN_UTILIZATION, String.valueOf(config.getDBCleanerMinUtilization())) 663 .setConfigParam(LOG_FILE_MAX, String.valueOf(config.getDBLogFileMax())); 664 } 665 else 666 { 667 envConfig = ConfigurableEnvironment.parseConfigEntry(config); 668 } 669 670 diskMonitor = serverContext.getDiskSpaceMonitor(); 671 memQuota = serverContext.getMemoryQuota(); 672 if (config.getDBCacheSize() > 0) 673 { 674 memQuota.acquireMemory(config.getDBCacheSize()); 675 } 676 else 677 { 678 memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent())); 679 } 680 } 681 682 private DatabaseConfig dbConfig() 683 { 684 boolean isImport = !envConfig.getTransactional(); 685 return new DatabaseConfig() 686 .setKeyPrefixing(true) 687 .setAllowCreate(true) 688 .setTransactional(!isImport) 689 .setDeferredWrite(isImport); 690 } 691 692 @Override 693 public void close() 694 { 695 synchronized (trees) 696 { 697 closeSilently(trees.values()); 698 trees.clear(); 699 } 700 701 if (env != null) 702 { 703 DirectoryServer.deregisterMonitorProvider(monitor); 704 monitor = null; 705 try 706 { 707 env.close(); 708 env = null; 709 } 710 catch (DatabaseException e) 711 { 712 throw new IllegalStateException(e); 713 } 714 } 715 716 if (config.getDBCacheSize() > 0) 717 { 718 memQuota.releaseMemory(config.getDBCacheSize()); 719 } 720 else 721 { 722 memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent())); 723 } 724 config.removeJEChangeListener(this); 725 diskMonitor.deregisterMonitoredDirectory(getDirectory(), this); 726 } 727 728 @Override 729 public void open(AccessMode accessMode) throws ConfigException, StorageRuntimeException 730 { 731 Reject.ifNull(accessMode, "accessMode must not be null"); 732 buildConfiguration(accessMode, false); 733 open0(); 734 } 735 736 private void open0() throws ConfigException 737 { 738 setupStorageFiles(backendDirectory, config.getDBDirectoryPermissions(), config.dn()); 739 try 740 { 741 if (env != null) 742 { 743 throw new IllegalStateException( 744 "Database is already open, either the backend is enabled or an import is currently running."); 745 } 746 env = new Environment(backendDirectory, envConfig); 747 monitor = new JEMonitor(config.getBackendId() + " JE Database", env); 748 DirectoryServer.registerMonitorProvider(monitor); 749 } 750 catch (DatabaseException e) 751 { 752 throw new StorageRuntimeException(e); 753 } 754 registerMonitoredDirectory(config); 755 } 756 757 @Override 758 public <T> T read(final ReadOperation<T> operation) throws Exception 759 { 760 try 761 { 762 return operation.run(newWriteableTransaction(null)); 763 } 764 catch (final StorageRuntimeException e) 765 { 766 if (e.getCause() != null) 767 { 768 throw (Exception) e.getCause(); 769 } 770 throw e; 771 } 772 } 773 774 @Override 775 public Importer startImport() throws ConfigException, StorageRuntimeException 776 { 777 buildConfiguration(AccessMode.READ_WRITE, true); 778 open0(); 779 return new ImporterImpl(); 780 } 781 782 private static String toDatabaseName(final TreeName treeName) 783 { 784 return treeName.toString(); 785 } 786 787 @Override 788 public void write(final WriteOperation operation) throws Exception 789 { 790 final Transaction txn = beginTransaction(); 791 try 792 { 793 operation.run(newWriteableTransaction(txn)); 794 commit(txn); 795 } 796 catch (final StorageRuntimeException e) 797 { 798 if (e.getCause() != null) 799 { 800 throw (Exception) e.getCause(); 801 } 802 throw e; 803 } 804 finally 805 { 806 abort(txn); 807 } 808 } 809 810 private Transaction beginTransaction() 811 { 812 if (envConfig.getTransactional()) 813 { 814 final Transaction txn = env.beginTransaction(null, TXN_READ_COMMITTED); 815 logger.trace("beginTransaction txnid=%d", txn.getId()); 816 return txn; 817 } 818 return null; 819 } 820 821 private void commit(final Transaction txn) 822 { 823 if (txn != null) 824 { 825 txn.commit(); 826 logger.trace("commit txnid=%d", txn.getId()); 827 } 828 } 829 830 private void abort(final Transaction txn) 831 { 832 if (txn != null) 833 { 834 txn.abort(); 835 logger.trace("abort txnid=%d", txn.getId()); 836 } 837 } 838 839 @Override 840 public boolean supportsBackupAndRestore() 841 { 842 return true; 843 } 844 845 @Override 846 public File getDirectory() 847 { 848 return getBackendDirectory(config); 849 } 850 851 private static File getBackendDirectory(JEBackendCfg cfg) 852 { 853 return getDBDirectory(cfg.getDBDirectory(), cfg.getBackendId()); 854 } 855 856 @Override 857 public ListIterator<Path> getFilesToBackup() throws DirectoryException 858 { 859 return new JELogFilesIterator(getDirectory(), config.getBackendId()); 860 } 861 862 /** 863 * Iterator on JE log files to backup. 864 * <p> 865 * The cleaner thread may delete some log files during the backup. The iterator is automatically 866 * renewed if at least one file has been deleted. 867 */ 868 static class JELogFilesIterator implements ListIterator<Path> 869 { 870 /** Root directory where all files are located. */ 871 private final File rootDirectory; 872 private final String backendID; 873 874 /** Underlying iterator on files. */ 875 private ListIterator<Path> iterator; 876 /** Files to backup. Used to renew the iterator if necessary. */ 877 private List<Path> files; 878 879 private String lastFileName = ""; 880 private long lastFileSize; 881 882 JELogFilesIterator(File rootDirectory, String backendID) throws DirectoryException 883 { 884 this.rootDirectory = rootDirectory; 885 this.backendID = backendID; 886 setFiles(BackupManager.getFiles(rootDirectory, new JELogFileFilter(), backendID)); 887 } 888 889 private void setFiles(List<Path> files) 890 { 891 this.files = files; 892 Collections.sort(files); 893 if (!files.isEmpty()) 894 { 895 Path lastFile = files.get(files.size() - 1); 896 lastFileName = lastFile.getFileName().toString(); 897 lastFileSize = lastFile.toFile().length(); 898 } 899 iterator = files.listIterator(); 900 } 901 902 @Override 903 public boolean hasNext() 904 { 905 boolean hasNext = iterator.hasNext(); 906 if (!hasNext && !files.isEmpty()) 907 { 908 try 909 { 910 List<Path> allFiles = BackupManager.getFiles(rootDirectory, new JELogFileFilter(), backendID); 911 List<Path> compare = new ArrayList<>(files); 912 compare.removeAll(allFiles); 913 if (!compare.isEmpty()) 914 { 915 // at least one file was deleted, 916 // the iterator must be renewed based on last file previously available 917 List<Path> newFiles = 918 BackupManager.getFiles(rootDirectory, new JELogFileFilter(lastFileName, lastFileSize), backendID); 919 logger.info(NOTE_JEB_BACKUP_CLEANER_ACTIVITY.get(newFiles.size())); 920 if (!newFiles.isEmpty()) 921 { 922 setFiles(newFiles); 923 hasNext = iterator.hasNext(); 924 } 925 } 926 } 927 catch (DirectoryException e) 928 { 929 logger.error(ERR_BACKEND_LIST_FILES_TO_BACKUP.get(backendID, stackTraceToSingleLineString(e))); 930 } 931 } 932 return hasNext; 933 } 934 935 @Override 936 public Path next() 937 { 938 if (hasNext()) 939 { 940 return iterator.next(); 941 } 942 throw new NoSuchElementException(); 943 } 944 945 @Override 946 public boolean hasPrevious() 947 { 948 return iterator.hasPrevious(); 949 } 950 951 @Override 952 public Path previous() 953 { 954 return iterator.previous(); 955 } 956 957 @Override 958 public int nextIndex() 959 { 960 return iterator.nextIndex(); 961 } 962 963 @Override 964 public int previousIndex() 965 { 966 return iterator.previousIndex(); 967 } 968 969 @Override 970 public void remove() 971 { 972 throw new UnsupportedOperationException("remove() is not implemented"); 973 } 974 975 @Override 976 public void set(Path e) 977 { 978 throw new UnsupportedOperationException("set() is not implemented"); 979 } 980 981 @Override 982 public void add(Path e) 983 { 984 throw new UnsupportedOperationException("add() is not implemented"); 985 } 986 } 987 988 /** 989 * This class implements a FilenameFilter to detect a JE log file, possibly with a constraint on 990 * the file name and file size. 991 */ 992 private static class JELogFileFilter implements FileFilter 993 { 994 private final String latestFilename; 995 private final long latestFileSize; 996 997 /** 998 * Creates the filter for log files that are newer than provided file name 999 * or equal to provided file name and of larger size. 1000 * @param latestFilename the latest file name 1001 * @param latestFileSize the latest file size 1002 */ 1003 JELogFileFilter(String latestFilename, long latestFileSize) 1004 { 1005 this.latestFilename = latestFilename; 1006 this.latestFileSize = latestFileSize; 1007 } 1008 1009 /** Creates the filter for any JE log file. */ 1010 JELogFileFilter() 1011 { 1012 this("", 0); 1013 } 1014 1015 @Override 1016 public boolean accept(File file) 1017 { 1018 String name = file.getName(); 1019 int cmp = name.compareTo(latestFilename); 1020 return name.endsWith(".jdb") 1021 && (cmp > 0 || (cmp == 0 && file.length() > latestFileSize)); 1022 } 1023 } 1024 1025 @Override 1026 public Path beforeRestore() throws DirectoryException 1027 { 1028 return null; 1029 } 1030 1031 @Override 1032 public boolean isDirectRestore() 1033 { 1034 // restore is done in an intermediate directory 1035 return false; 1036 } 1037 1038 @Override 1039 public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException 1040 { 1041 // intermediate directory content is moved to database directory 1042 File targetDirectory = getDirectory(); 1043 recursiveDelete(targetDirectory); 1044 try 1045 { 1046 Files.move(restoreDirectory, targetDirectory.toPath()); 1047 } 1048 catch(IOException e) 1049 { 1050 LocalizableMessage msg = ERR_CANNOT_RENAME_RESTORE_DIRECTORY.get(restoreDirectory, targetDirectory.getPath()); 1051 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); 1052 } 1053 } 1054 1055 @Override 1056 public void createBackup(BackupConfig backupConfig) throws DirectoryException 1057 { 1058 new BackupManager(config.getBackendId()).createBackup(this, backupConfig); 1059 } 1060 1061 @Override 1062 public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException 1063 { 1064 new BackupManager(config.getBackendId()).removeBackup(backupDirectory, backupID); 1065 } 1066 1067 @Override 1068 public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException 1069 { 1070 new BackupManager(config.getBackendId()).restoreBackup(this, restoreConfig); 1071 } 1072 1073 @Override 1074 public Set<TreeName> listTrees() 1075 { 1076 try 1077 { 1078 List<String> treeNames = env.getDatabaseNames(); 1079 final Set<TreeName> results = new HashSet<>(treeNames.size()); 1080 for (String treeName : treeNames) 1081 { 1082 results.add(TreeName.valueOf(treeName)); 1083 } 1084 return results; 1085 } 1086 catch (DatabaseException e) 1087 { 1088 throw new StorageRuntimeException(e); 1089 } 1090 } 1091 1092 @Override 1093 public boolean isConfigurationChangeAcceptable(JEBackendCfg newCfg, 1094 List<LocalizableMessage> unacceptableReasons) 1095 { 1096 long newSize = computeSize(newCfg); 1097 long oldSize = computeSize(config); 1098 return (newSize <= oldSize || memQuota.isMemoryAvailable(newSize - oldSize)) 1099 && checkConfigurationDirectories(newCfg, unacceptableReasons); 1100 } 1101 1102 private long computeSize(JEBackendCfg cfg) 1103 { 1104 return cfg.getDBCacheSize() > 0 ? cfg.getDBCacheSize() : memQuota.memPercentToBytes(cfg.getDBCachePercent()); 1105 } 1106 1107 /** 1108 * Checks newly created backend has a valid configuration. 1109 * @param cfg the new configuration 1110 * @param unacceptableReasons the list of accumulated errors and their messages 1111 * @param context the server context 1112 * @return true if newly created backend has a valid configuration 1113 */ 1114 static boolean isConfigurationAcceptable(JEBackendCfg cfg, List<LocalizableMessage> unacceptableReasons, 1115 ServerContext context) 1116 { 1117 if (context != null) 1118 { 1119 MemoryQuota memQuota = context.getMemoryQuota(); 1120 if (cfg.getDBCacheSize() > 0 && !memQuota.isMemoryAvailable(cfg.getDBCacheSize())) 1121 { 1122 unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get( 1123 cfg.getDBCacheSize(), memQuota.getAvailableMemory())); 1124 return false; 1125 } 1126 else if (!memQuota.isMemoryAvailable(memQuota.memPercentToBytes(cfg.getDBCachePercent()))) 1127 { 1128 unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_PERCENT_GREATER_THAN_JVM_HEAP.get( 1129 cfg.getDBCachePercent(), memQuota.memBytesToPercent(memQuota.getAvailableMemory()))); 1130 return false; 1131 } 1132 } 1133 return checkConfigurationDirectories(cfg, unacceptableReasons); 1134 } 1135 1136 private static boolean checkConfigurationDirectories(JEBackendCfg cfg, 1137 List<LocalizableMessage> unacceptableReasons) 1138 { 1139 final ConfigChangeResult ccr = new ConfigChangeResult(); 1140 File newBackendDirectory = getBackendDirectory(cfg); 1141 1142 checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, true); 1143 checkDBDirPermissions(cfg.getDBDirectoryPermissions(), cfg.dn(), ccr); 1144 if (!ccr.getMessages().isEmpty()) 1145 { 1146 unacceptableReasons.addAll(ccr.getMessages()); 1147 return false; 1148 } 1149 return true; 1150 } 1151 1152 @Override 1153 public ConfigChangeResult applyConfigurationChange(JEBackendCfg cfg) 1154 { 1155 final ConfigChangeResult ccr = new ConfigChangeResult(); 1156 1157 try 1158 { 1159 File newBackendDirectory = getBackendDirectory(cfg); 1160 1161 // Create the directory if it doesn't exist. 1162 if (!cfg.getDBDirectory().equals(config.getDBDirectory())) 1163 { 1164 checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, false); 1165 if (!ccr.getMessages().isEmpty()) 1166 { 1167 return ccr; 1168 } 1169 1170 ccr.setAdminActionRequired(true); 1171 ccr.addMessage(NOTE_CONFIG_DB_DIR_REQUIRES_RESTART.get(config.getDBDirectory(), cfg.getDBDirectory())); 1172 } 1173 1174 if (!cfg.getDBDirectoryPermissions().equalsIgnoreCase(config.getDBDirectoryPermissions()) 1175 || !cfg.getDBDirectory().equals(config.getDBDirectory())) 1176 { 1177 checkDBDirPermissions(cfg.getDBDirectoryPermissions(), cfg.dn(), ccr); 1178 if (!ccr.getMessages().isEmpty()) 1179 { 1180 return ccr; 1181 } 1182 1183 setDBDirPermissions(newBackendDirectory, cfg.getDBDirectoryPermissions(), cfg.dn(), ccr); 1184 if (!ccr.getMessages().isEmpty()) 1185 { 1186 return ccr; 1187 } 1188 } 1189 registerMonitoredDirectory(cfg); 1190 config = cfg; 1191 } 1192 catch (Exception e) 1193 { 1194 addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e))); 1195 } 1196 return ccr; 1197 } 1198 1199 private void registerMonitoredDirectory(JEBackendCfg cfg) 1200 { 1201 diskMonitor.registerMonitoredDirectory( 1202 cfg.getBackendId() + " backend", 1203 getDirectory(), 1204 cfg.getDiskLowThreshold(), 1205 cfg.getDiskFullThreshold(), 1206 this); 1207 } 1208 1209 @Override 1210 public void removeStorageFiles() throws StorageRuntimeException 1211 { 1212 StorageUtils.removeStorageFiles(backendDirectory); 1213 } 1214 1215 @Override 1216 public StorageStatus getStorageStatus() 1217 { 1218 return storageStatus; 1219 } 1220 1221 @Override 1222 public void diskFullThresholdReached(File directory, long thresholdInBytes) { 1223 storageStatus = statusWhenDiskSpaceFull(directory, thresholdInBytes, config.getBackendId()); 1224 } 1225 1226 @Override 1227 public void diskLowThresholdReached(File directory, long thresholdInBytes) { 1228 storageStatus = statusWhenDiskSpaceLow(directory, thresholdInBytes, config.getBackendId()); 1229 } 1230 1231 @Override 1232 public void diskSpaceRestored(File directory, long lowThresholdInBytes, long fullThresholdInBytes) { 1233 storageStatus = StorageStatus.working(); 1234 } 1235 1236 private static void setData(final DatabaseEntry dbEntry, final ByteSequence bs) 1237 { 1238 dbEntry.setData(bs != null ? bs.toByteArray() : null); 1239 } 1240 1241 private static DatabaseEntry db(final ByteSequence bs) 1242 { 1243 return new DatabaseEntry(bs != null ? bs.toByteArray() : null); 1244 } 1245 1246 private static ByteString valueToBytes(final DatabaseEntry dbValue, boolean isDefined) 1247 { 1248 if (isDefined) 1249 { 1250 return ByteString.wrap(dbValue.getData()); 1251 } 1252 return null; 1253 } 1254}