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