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 2014-2015 ForgeRock AS 025 */ 026package org.opends.server.backends.pdb; 027 028import static com.persistit.Transaction.CommitPolicy.*; 029import static java.util.Arrays.*; 030 031import static org.opends.messages.BackendMessages.*; 032import static org.opends.messages.ConfigMessages.*; 033import static org.opends.messages.UtilityMessages.*; 034import static org.opends.server.util.StaticUtils.*; 035 036import java.io.Closeable; 037import java.io.File; 038import java.io.FileFilter; 039import java.io.IOException; 040import java.nio.file.Files; 041import java.nio.file.Path; 042import java.nio.file.Paths; 043import java.rmi.RemoteException; 044import java.util.ArrayList; 045import java.util.HashMap; 046import java.util.HashSet; 047import java.util.List; 048import java.util.ListIterator; 049import java.util.Map; 050import java.util.NoSuchElementException; 051import java.util.Objects; 052import java.util.Queue; 053import java.util.Set; 054import java.util.concurrent.ConcurrentLinkedDeque; 055 056import org.forgerock.i18n.LocalizableMessage; 057import org.forgerock.i18n.slf4j.LocalizedLogger; 058import org.forgerock.opendj.config.server.ConfigChangeResult; 059import org.forgerock.opendj.config.server.ConfigException; 060import org.forgerock.opendj.ldap.ByteSequence; 061import org.forgerock.opendj.ldap.ByteString; 062import org.forgerock.util.Reject; 063import org.opends.server.admin.server.ConfigurationChangeListener; 064import org.opends.server.admin.std.server.PDBBackendCfg; 065import org.opends.server.api.Backupable; 066import org.opends.server.api.DiskSpaceMonitorHandler; 067import org.opends.server.backends.pluggable.spi.AccessMode; 068import org.opends.server.backends.pluggable.spi.Cursor; 069import org.opends.server.backends.pluggable.spi.Importer; 070import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException; 071import org.opends.server.backends.pluggable.spi.ReadOperation; 072import org.opends.server.backends.pluggable.spi.Storage; 073import org.opends.server.backends.pluggable.spi.StorageInUseException; 074import org.opends.server.backends.pluggable.spi.StorageRuntimeException; 075import org.opends.server.backends.pluggable.spi.StorageStatus; 076import org.opends.server.backends.pluggable.spi.TreeName; 077import org.opends.server.backends.pluggable.spi.UpdateFunction; 078import org.opends.server.backends.pluggable.spi.WriteOperation; 079import org.opends.server.backends.pluggable.spi.WriteableTransaction; 080import org.opends.server.core.DirectoryServer; 081import org.opends.server.core.MemoryQuota; 082import org.opends.server.core.ServerContext; 083import org.opends.server.extensions.DiskSpaceMonitor; 084import org.opends.server.types.BackupConfig; 085import org.opends.server.types.BackupDirectory; 086import org.opends.server.types.DirectoryException; 087import org.opends.server.types.FilePermission; 088import org.opends.server.types.RestoreConfig; 089import org.opends.server.util.BackupManager; 090 091import com.persistit.Configuration; 092import com.persistit.Configuration.BufferPoolConfiguration; 093import com.persistit.Exchange; 094import com.persistit.Key; 095import com.persistit.Persistit; 096import com.persistit.Transaction; 097import com.persistit.Tree; 098import com.persistit.Value; 099import com.persistit.Volume; 100import com.persistit.VolumeSpecification; 101import com.persistit.exception.InUseException; 102import com.persistit.exception.PersistitException; 103import com.persistit.exception.RollbackException; 104import com.persistit.exception.TreeNotFoundException; 105 106/** PersistIt database implementation of the {@link Storage} engine. */ 107@SuppressWarnings("javadoc") 108public final class PDBStorage implements Storage, Backupable, ConfigurationChangeListener<PDBBackendCfg>, 109 DiskSpaceMonitorHandler 110{ 111 private static final double MAX_SLEEP_ON_RETRY_MS = 50.0; 112 113 private static final String VOLUME_NAME = "dj"; 114 115 private static final String JOURNAL_NAME = VOLUME_NAME + "_journal"; 116 117 /** The buffer / page size used by the PersistIt storage. */ 118 private static final int BUFFER_SIZE = 16 * 1024; 119 120 /** PersistIt implementation of the {@link Cursor} interface. */ 121 private static final class CursorImpl implements Cursor<ByteString, ByteString> 122 { 123 private ByteString currentKey; 124 private ByteString currentValue; 125 private final Exchange exchange; 126 127 private CursorImpl(final Exchange exchange) 128 { 129 this.exchange = exchange; 130 } 131 132 @Override 133 public void close() 134 { 135 // Release immediately because this exchange did not come from the txn cache 136 exchange.getPersistitInstance().releaseExchange(exchange); 137 } 138 139 @Override 140 public boolean isDefined() { 141 return exchange.getValue().isDefined(); 142 } 143 144 @Override 145 public ByteString getKey() 146 { 147 if (currentKey == null) 148 { 149 throwIfUndefined(); 150 currentKey = ByteString.wrap(exchange.getKey().reset().decodeByteArray()); 151 } 152 return currentKey; 153 } 154 155 @Override 156 public ByteString getValue() 157 { 158 if (currentValue == null) 159 { 160 throwIfUndefined(); 161 currentValue = ByteString.wrap(exchange.getValue().getByteArray()); 162 } 163 return currentValue; 164 } 165 166 @Override 167 public boolean next() 168 { 169 clearCurrentKeyAndValue(); 170 try 171 { 172 return exchange.next(); 173 } 174 catch (final PersistitException e) 175 { 176 throw new StorageRuntimeException(e); 177 } 178 } 179 180 @Override 181 public boolean positionToKey(final ByteSequence key) 182 { 183 clearCurrentKeyAndValue(); 184 bytesToKey(exchange.getKey(), key); 185 try 186 { 187 exchange.fetch(); 188 return exchange.getValue().isDefined(); 189 } 190 catch (final PersistitException e) 191 { 192 throw new StorageRuntimeException(e); 193 } 194 } 195 196 @Override 197 public boolean positionToKeyOrNext(final ByteSequence key) 198 { 199 clearCurrentKeyAndValue(); 200 bytesToKey(exchange.getKey(), key); 201 try 202 { 203 exchange.fetch(); 204 return exchange.getValue().isDefined() || exchange.next(); 205 } 206 catch (final PersistitException e) 207 { 208 throw new StorageRuntimeException(e); 209 } 210 } 211 212 @Override 213 public boolean positionToIndex(int index) 214 { 215 // There doesn't seem to be a way to optimize this using Persistit. 216 clearCurrentKeyAndValue(); 217 exchange.getKey().to(Key.BEFORE); 218 try 219 { 220 for (int i = 0; i <= index; i++) 221 { 222 if (!exchange.next()) 223 { 224 return false; 225 } 226 } 227 return true; 228 } 229 catch (final PersistitException e) 230 { 231 throw new StorageRuntimeException(e); 232 } 233 } 234 235 @Override 236 public boolean positionToLastKey() 237 { 238 clearCurrentKeyAndValue(); 239 exchange.getKey().to(Key.AFTER); 240 try 241 { 242 return exchange.previous(); 243 } 244 catch (final PersistitException e) 245 { 246 throw new StorageRuntimeException(e); 247 } 248 } 249 250 private void clearCurrentKeyAndValue() 251 { 252 currentKey = null; 253 currentValue = null; 254 } 255 256 private void throwIfUndefined() { 257 if (!isDefined()) { 258 throw new NoSuchElementException(); 259 } 260 } 261 } 262 263 /** PersistIt implementation of the {@link Importer} interface. */ 264 private final class ImporterImpl implements Importer 265 { 266 private final Map<TreeName, Tree> trees = new HashMap<>(); 267 private final Queue<Map<TreeName, Exchange>> allExchanges = new ConcurrentLinkedDeque<>(); 268 private final ThreadLocal<Map<TreeName, Exchange>> exchanges = new ThreadLocal<Map<TreeName, Exchange>>() 269 { 270 @Override 271 protected Map<TreeName, Exchange> initialValue() 272 { 273 final Map<TreeName, Exchange> value = new HashMap<>(); 274 allExchanges.add(value); 275 return value; 276 } 277 }; 278 279 @Override 280 public void close() 281 { 282 for (Map<TreeName, Exchange> map : allExchanges) 283 { 284 for (Exchange exchange : map.values()) 285 { 286 db.releaseExchange(exchange); 287 } 288 map.clear(); 289 } 290 PDBStorage.this.close(); 291 } 292 293 @Override 294 public void createTree(final TreeName treeName) 295 { 296 try 297 { 298 final Tree tree = volume.getTree(mangleTreeName(treeName), true); 299 trees.put(treeName, tree); 300 } 301 catch (final PersistitException e) 302 { 303 throw new StorageRuntimeException(e); 304 } 305 } 306 307 @Override 308 public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value) 309 { 310 try 311 { 312 final Exchange ex = getExchangeFromCache(treeName); 313 bytesToKey(ex.getKey(), key); 314 bytesToValue(ex.getValue(), value); 315 ex.store(); 316 } 317 catch (final Exception e) 318 { 319 throw new StorageRuntimeException(e); 320 } 321 } 322 323 @Override 324 public boolean delete(final TreeName treeName, final ByteSequence key) 325 { 326 try 327 { 328 final Exchange ex = getExchangeFromCache(treeName); 329 bytesToKey(ex.getKey(), key); 330 return ex.remove(); 331 } 332 catch (final PersistitException e) 333 { 334 throw new StorageRuntimeException(e); 335 } 336 } 337 338 @Override 339 public ByteString read(final TreeName treeName, final ByteSequence key) 340 { 341 try 342 { 343 final Exchange ex = getExchangeFromCache(treeName); 344 bytesToKey(ex.getKey(), key); 345 ex.fetch(); 346 return valueToBytes(ex.getValue()); 347 } 348 catch (final PersistitException e) 349 { 350 throw new StorageRuntimeException(e); 351 } 352 } 353 354 private Exchange getExchangeFromCache(final TreeName treeName) throws PersistitException 355 { 356 Map<TreeName, Exchange> threadExchanges = exchanges.get(); 357 Exchange exchange = threadExchanges.get(treeName); 358 if (exchange == null) 359 { 360 exchange = getNewExchange(treeName, false); 361 threadExchanges.put(treeName, exchange); 362 } 363 return exchange; 364 } 365 } 366 367 /** Common interface for internal WriteableTransaction implementations. */ 368 private interface StorageImpl extends WriteableTransaction, Closeable { 369 370 } 371 372 /** PersistIt implementation of the {@link WriteableTransaction} interface. */ 373 private final class WriteableStorageImpl implements StorageImpl 374 { 375 private final Map<TreeName, Exchange> exchanges = new HashMap<>(); 376 private final String DUMMY_RECORD = "_DUMMY_RECORD_"; 377 378 @Override 379 public void put(final TreeName treeName, final ByteSequence key, final ByteSequence value) 380 { 381 try 382 { 383 final Exchange ex = getExchangeFromCache(treeName); 384 bytesToKey(ex.getKey(), key); 385 bytesToValue(ex.getValue(), value); 386 ex.store(); 387 } 388 catch (final Exception e) 389 { 390 throw new StorageRuntimeException(e); 391 } 392 } 393 394 @Override 395 public boolean delete(final TreeName treeName, final ByteSequence key) 396 { 397 try 398 { 399 final Exchange ex = getExchangeFromCache(treeName); 400 bytesToKey(ex.getKey(), key); 401 return ex.remove(); 402 } 403 catch (final PersistitException e) 404 { 405 throw new StorageRuntimeException(e); 406 } 407 } 408 409 @Override 410 public void deleteTree(final TreeName treeName) 411 { 412 Exchange ex = null; 413 try 414 { 415 ex = getExchangeFromCache(treeName); 416 ex.removeTree(); 417 } 418 catch (final PersistitException e) 419 { 420 throw new StorageRuntimeException(e); 421 } 422 finally 423 { 424 exchanges.values().remove(ex); 425 db.releaseExchange(ex); 426 } 427 } 428 429 @Override 430 public long getRecordCount(TreeName treeName) 431 { 432 // FIXME: is there a better/quicker way to do this? 433 final Cursor<?, ?> cursor = openCursor(treeName); 434 try 435 { 436 long count = 0; 437 while (cursor.next()) 438 { 439 count++; 440 } 441 return count; 442 } 443 finally 444 { 445 cursor.close(); 446 } 447 } 448 449 @Override 450 public Cursor<ByteString, ByteString> openCursor(final TreeName treeName) 451 { 452 try 453 { 454 /* 455 * Acquire a new exchange for the cursor rather than using a cached 456 * exchange in order to avoid reentrant accesses to the same tree 457 * interfering with the cursor position. 458 */ 459 return new CursorImpl(getNewExchange(treeName, false)); 460 } 461 catch (final PersistitException e) 462 { 463 throw new StorageRuntimeException(e); 464 } 465 } 466 467 @Override 468 public void openTree(final TreeName treeName, boolean createOnDemand) 469 { 470 if (createOnDemand) 471 { 472 openCreateTree(treeName); 473 } 474 else 475 { 476 try 477 { 478 getExchangeFromCache(treeName); 479 } 480 catch (final PersistitException e) 481 { 482 throw new StorageRuntimeException(e); 483 } 484 } 485 } 486 487 @Override 488 public ByteString read(final TreeName treeName, final ByteSequence key) 489 { 490 try 491 { 492 final Exchange ex = getExchangeFromCache(treeName); 493 bytesToKey(ex.getKey(), key); 494 ex.fetch(); 495 return valueToBytes(ex.getValue()); 496 } 497 catch (final PersistitException e) 498 { 499 throw new StorageRuntimeException(e); 500 } 501 } 502 503 @Override 504 public void renameTree(final TreeName oldTreeName, final TreeName newTreeName) 505 { 506 throw new UnsupportedOperationException(); 507 } 508 509 @Override 510 public boolean update(final TreeName treeName, final ByteSequence key, final UpdateFunction f) 511 { 512 try 513 { 514 final Exchange ex = getExchangeFromCache(treeName); 515 bytesToKey(ex.getKey(), key); 516 ex.fetch(); 517 final ByteSequence oldValue = valueToBytes(ex.getValue()); 518 final ByteSequence newValue = f.computeNewValue(oldValue); 519 if (!Objects.equals(newValue, oldValue)) 520 { 521 if (newValue == null) 522 { 523 ex.remove(); 524 } 525 else 526 { 527 ex.getValue().clear().putByteArray(newValue.toByteArray()); 528 ex.store(); 529 } 530 return true; 531 } 532 return false; 533 } 534 catch (final Exception e) 535 { 536 throw new StorageRuntimeException(e); 537 } 538 } 539 540 private void openCreateTree(final TreeName treeName) 541 { 542 Exchange ex = null; 543 try 544 { 545 ex = getNewExchange(treeName, true); 546 // Work around a problem with forced shutdown right after tree creation. 547 // Tree operations are not part of the journal, so force a couple operations to be able to recover. 548 ByteString dummyKey = ByteString.valueOf(DUMMY_RECORD); 549 put(treeName, dummyKey, ByteString.empty()); 550 delete(treeName, dummyKey); 551 } 552 catch (final PersistitException e) 553 { 554 throw new StorageRuntimeException(e); 555 } 556 finally 557 { 558 db.releaseExchange(ex); 559 } 560 } 561 562 private Exchange getExchangeFromCache(final TreeName treeName) throws PersistitException 563 { 564 Exchange exchange = exchanges.get(treeName); 565 if (exchange == null) 566 { 567 exchange = getNewExchange(treeName, false); 568 exchanges.put(treeName, exchange); 569 } 570 return exchange; 571 } 572 573 @Override 574 public void close() 575 { 576 for (final Exchange ex : exchanges.values()) 577 { 578 db.releaseExchange(ex); 579 } 580 exchanges.clear(); 581 } 582 } 583 584 /** PersistIt read-only implementation of {@link StorageImpl} interface. */ 585 private final class ReadOnlyStorageImpl implements StorageImpl { 586 587 private final WriteableStorageImpl delegate; 588 589 ReadOnlyStorageImpl(WriteableStorageImpl delegate) 590 { 591 this.delegate = delegate; 592 } 593 594 @Override 595 public ByteString read(TreeName treeName, ByteSequence key) 596 { 597 return delegate.read(treeName, key); 598 } 599 600 @Override 601 public Cursor<ByteString, ByteString> openCursor(TreeName treeName) 602 { 603 return delegate.openCursor(treeName); 604 } 605 606 @Override 607 public long getRecordCount(TreeName treeName) 608 { 609 return delegate.getRecordCount(treeName); 610 } 611 612 @Override 613 public void openTree(TreeName treeName, boolean createOnDemand) 614 { 615 if (createOnDemand) 616 { 617 throw new ReadOnlyStorageException(); 618 } 619 Exchange ex = null; 620 try 621 { 622 ex = getNewExchange(treeName, false); 623 } 624 catch (final TreeNotFoundException e) 625 { 626 // ignore missing trees. 627 } 628 catch (final PersistitException e) 629 { 630 throw new StorageRuntimeException(e); 631 } 632 finally 633 { 634 db.releaseExchange(ex); 635 } 636 } 637 638 @Override 639 public void close() 640 { 641 delegate.close(); 642 } 643 644 @Override 645 public void renameTree(TreeName oldName, TreeName newName) 646 { 647 throw new ReadOnlyStorageException(); 648 } 649 650 @Override 651 public void deleteTree(TreeName name) 652 { 653 throw new ReadOnlyStorageException(); 654 } 655 656 @Override 657 public void put(TreeName treeName, ByteSequence key, ByteSequence value) 658 { 659 throw new ReadOnlyStorageException(); 660 } 661 662 @Override 663 public boolean update(TreeName treeName, ByteSequence key, UpdateFunction f) 664 { 665 throw new ReadOnlyStorageException(); 666 } 667 668 @Override 669 public boolean delete(TreeName treeName, ByteSequence key) 670 { 671 throw new ReadOnlyStorageException(); 672 } 673 } 674 675 private Exchange getNewExchange(final TreeName treeName, final boolean create) throws PersistitException 676 { 677 return db.getExchange(volume, mangleTreeName(treeName), create); 678 } 679 680 private StorageImpl newStorageImpl() { 681 final WriteableStorageImpl writeableStorage = new WriteableStorageImpl(); 682 return accessMode.isWriteable() ? writeableStorage : new ReadOnlyStorageImpl(writeableStorage); 683 } 684 685 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 686 private final ServerContext serverContext; 687 private final File backendDirectory; 688 private AccessMode accessMode; 689 private Persistit db; 690 private Volume volume; 691 private PDBBackendCfg config; 692 private DiskSpaceMonitor diskMonitor; 693 private PDBMonitor pdbMonitor; 694 private MemoryQuota memQuota; 695 private StorageStatus storageStatus = StorageStatus.working(); 696 697 /** 698 * Creates a new persistit storage with the provided configuration. 699 * 700 * @param cfg 701 * The configuration. 702 * @param serverContext 703 * This server instance context 704 * @throws ConfigException if memory cannot be reserved 705 */ 706 // FIXME: should be package private once importer is decoupled. 707 public PDBStorage(final PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException 708 { 709 this.serverContext = serverContext; 710 backendDirectory = new File(getFileForPath(cfg.getDBDirectory()), cfg.getBackendId()); 711 config = cfg; 712 cfg.addPDBChangeListener(this); 713 } 714 715 private Configuration buildConfiguration(AccessMode accessMode) 716 { 717 this.accessMode = accessMode; 718 719 final Configuration dbCfg = new Configuration(); 720 dbCfg.setLogFile(new File(backendDirectory, VOLUME_NAME + ".log").getPath()); 721 dbCfg.setJournalPath(new File(backendDirectory, JOURNAL_NAME).getPath()); 722 dbCfg.setCheckpointInterval(config.getDBCheckpointerWakeupInterval()); 723 // Volume is opened read write because recovery will fail if opened read-only 724 dbCfg.setVolumeList(asList(new VolumeSpecification(new File(backendDirectory, VOLUME_NAME).getPath(), null, 725 BUFFER_SIZE, 4096, Long.MAX_VALUE / BUFFER_SIZE, 2048, true, false, false))); 726 final BufferPoolConfiguration bufferPoolCfg = getBufferPoolCfg(dbCfg); 727 bufferPoolCfg.setMaximumCount(Integer.MAX_VALUE); 728 729 diskMonitor = serverContext.getDiskSpaceMonitor(); 730 memQuota = serverContext.getMemoryQuota(); 731 if (config.getDBCacheSize() > 0) 732 { 733 bufferPoolCfg.setMaximumMemory(config.getDBCacheSize()); 734 memQuota.acquireMemory(config.getDBCacheSize()); 735 } 736 else 737 { 738 bufferPoolCfg.setMaximumMemory(memQuota.memPercentToBytes(config.getDBCachePercent())); 739 memQuota.acquireMemory(memQuota.memPercentToBytes(config.getDBCachePercent())); 740 } 741 dbCfg.setCommitPolicy(config.isDBTxnNoSync() ? SOFT : GROUP); 742 dbCfg.setJmxEnabled(false); 743 return dbCfg; 744 } 745 746 /** {@inheritDoc} */ 747 @Override 748 public void close() 749 { 750 if (db != null) 751 { 752 DirectoryServer.deregisterMonitorProvider(pdbMonitor); 753 pdbMonitor = null; 754 try 755 { 756 db.close(); 757 db = null; 758 } 759 catch (final PersistitException e) 760 { 761 throw new IllegalStateException(e); 762 } 763 } 764 if (config.getDBCacheSize() > 0) 765 { 766 memQuota.releaseMemory(config.getDBCacheSize()); 767 } 768 else 769 { 770 memQuota.releaseMemory(memQuota.memPercentToBytes(config.getDBCachePercent())); 771 } 772 config.removePDBChangeListener(this); 773 diskMonitor.deregisterMonitoredDirectory(getDirectory(), this); 774 } 775 776 private static BufferPoolConfiguration getBufferPoolCfg(Configuration dbCfg) 777 { 778 return dbCfg.getBufferPoolMap().get(BUFFER_SIZE); 779 } 780 781 /** {@inheritDoc} */ 782 @Override 783 public void open(AccessMode accessMode) throws ConfigException, StorageRuntimeException 784 { 785 Reject.ifNull(accessMode, "accessMode must not be null"); 786 open0(buildConfiguration(accessMode)); 787 } 788 789 private void open0(final Configuration dbCfg) throws ConfigException 790 { 791 setupStorageFiles(); 792 try 793 { 794 if (db != null) 795 { 796 throw new IllegalStateException( 797 "Database is already open, either the backend is enabled or an import is currently running."); 798 } 799 db = new Persistit(dbCfg); 800 801 final long bufferCount = getBufferPoolCfg(dbCfg).computeBufferCount(db.getAvailableHeap()); 802 final long totalSize = bufferCount * BUFFER_SIZE / 1024; 803 logger.info(NOTE_PDB_MEMORY_CFG, config.getBackendId(), bufferCount, BUFFER_SIZE, totalSize); 804 805 db.initialize(); 806 volume = db.loadVolume(VOLUME_NAME); 807 pdbMonitor = new PDBMonitor(config.getBackendId() + " PDB Database", db); 808 DirectoryServer.registerMonitorProvider(pdbMonitor); 809 } 810 catch(final InUseException e) { 811 throw new StorageInUseException(e); 812 } 813 catch (final PersistitException e) 814 { 815 throw new StorageRuntimeException(e); 816 } 817 diskMonitor.registerMonitoredDirectory( 818 config.getBackendId() + " backend", 819 getDirectory(), 820 config.getDiskLowThreshold(), 821 config.getDiskFullThreshold(), 822 this); 823 } 824 825 /** {@inheritDoc} */ 826 @Override 827 public <T> T read(final ReadOperation<T> operation) throws Exception 828 { 829 final Transaction txn = db.getTransaction(); 830 for (;;) 831 { 832 txn.begin(); 833 try 834 { 835 try (final StorageImpl storageImpl = newStorageImpl()) 836 { 837 final T result = operation.run(storageImpl); 838 txn.commit(); 839 return result; 840 } 841 catch (final StorageRuntimeException e) 842 { 843 if (e.getCause() != null) 844 { 845 throw (Exception) e.getCause(); 846 } 847 throw e; 848 } 849 } 850 catch (final RollbackException e) 851 { 852 // retry 853 } 854 catch (final Exception e) 855 { 856 txn.rollback(); 857 throw e; 858 } 859 finally 860 { 861 txn.end(); 862 } 863 } 864 } 865 866 /** {@inheritDoc} */ 867 @Override 868 public Importer startImport() throws ConfigException, StorageRuntimeException 869 { 870 open0(buildConfiguration(AccessMode.READ_WRITE)); 871 return new ImporterImpl(); 872 } 873 874 private static String mangleTreeName(final TreeName treeName) 875 { 876 StringBuilder mangled = new StringBuilder(); 877 String name = treeName.toString(); 878 879 for (int idx = 0; idx < name.length(); idx++) 880 { 881 char ch = name.charAt(idx); 882 if (ch == '=' || ch == ',') 883 { 884 ch = '_'; 885 } 886 mangled.append(ch); 887 } 888 return mangled.toString(); 889 } 890 891 /** {@inheritDoc} */ 892 @Override 893 public void write(final WriteOperation operation) throws Exception 894 { 895 final Transaction txn = db.getTransaction(); 896 for (;;) 897 { 898 txn.begin(); 899 try 900 { 901 try (final StorageImpl storageImpl = newStorageImpl()) 902 { 903 operation.run(storageImpl); 904 txn.commit(); 905 return; 906 } 907 catch (final StorageRuntimeException e) 908 { 909 if (e.getCause() != null) 910 { 911 throw (Exception) e.getCause(); 912 } 913 throw e; 914 } 915 } 916 catch (final RollbackException e) 917 { 918 // retry after random sleep (reduces transactions collision. Drawback: increased latency) 919 Thread.sleep((long) (Math.random() * MAX_SLEEP_ON_RETRY_MS)); 920 } 921 catch (final Exception e) 922 { 923 txn.rollback(); 924 throw e; 925 } 926 finally 927 { 928 txn.end(); 929 } 930 } 931 } 932 933 @Override 934 public boolean supportsBackupAndRestore() 935 { 936 return true; 937 } 938 939 @Override 940 public File getDirectory() 941 { 942 File parentDir = getFileForPath(config.getDBDirectory()); 943 return new File(parentDir, config.getBackendId()); 944 } 945 946 @Override 947 public ListIterator<Path> getFilesToBackup() throws DirectoryException 948 { 949 try 950 { 951 if (db == null) 952 { 953 return getFilesToBackupWhenOffline(); 954 } 955 956 // FIXME: use full programmatic way of retrieving backup file once available in persistIt 957 // When requesting files to backup, append only mode must also be set (-a) otherwise it will be ended 958 // by PersistIt and performing backup may corrupt the DB. 959 String filesAsString = db.getManagement().execute("backup -a -f"); 960 String[] allFiles = filesAsString.split("[\r\n]+"); 961 final List<Path> files = new ArrayList<>(); 962 for (String file : allFiles) 963 { 964 files.add(Paths.get(file)); 965 } 966 return files.listIterator(); 967 } 968 catch (Exception e) 969 { 970 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 971 ERR_BACKEND_LIST_FILES_TO_BACKUP.get(config.getBackendId(), stackTraceToSingleLineString(e))); 972 } 973 } 974 975 /** Filter to retrieve the database files to backup. */ 976 private static final FileFilter BACKUP_FILES_FILTER = new FileFilter() 977 { 978 @Override 979 public boolean accept(File file) 980 { 981 String name = file.getName(); 982 return name.equals(VOLUME_NAME) || name.matches(JOURNAL_NAME + "\\.\\d+$"); 983 } 984 }; 985 986 /** 987 * Returns the list of files to backup when there is no open database. 988 * <p> 989 * It is not possible to rely on the database returning the files, so the files must be retrieved 990 * from a file filter. 991 */ 992 private ListIterator<Path> getFilesToBackupWhenOffline() throws DirectoryException 993 { 994 return BackupManager.getFiles(getDirectory(), BACKUP_FILES_FILTER, config.getBackendId()).listIterator(); 995 } 996 997 @Override 998 public Path beforeRestore() throws DirectoryException 999 { 1000 return null; 1001 } 1002 1003 @Override 1004 public boolean isDirectRestore() 1005 { 1006 // restore is done in an intermediate directory 1007 return false; 1008 } 1009 1010 @Override 1011 public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException 1012 { 1013 // intermediate directory content is moved to database directory 1014 File targetDirectory = getDirectory(); 1015 recursiveDelete(targetDirectory); 1016 try 1017 { 1018 Files.move(restoreDirectory, targetDirectory.toPath()); 1019 } 1020 catch(IOException e) 1021 { 1022 LocalizableMessage msg = ERR_CANNOT_RENAME_RESTORE_DIRECTORY.get(restoreDirectory, targetDirectory.getPath()); 1023 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), msg); 1024 } 1025 } 1026 1027 /** 1028 * Switch the database in append only mode. 1029 * <p> 1030 * This is a mandatory operation before performing a backup. 1031 */ 1032 private void switchToAppendOnlyMode() throws DirectoryException 1033 { 1034 try 1035 { 1036 // FIXME: use full programmatic way of switching to this mode once available in persistIt 1037 db.getManagement().execute("backup -a -c"); 1038 } 1039 catch (RemoteException e) 1040 { 1041 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1042 ERR_BACKEND_SWITCH_TO_APPEND_MODE.get(config.getBackendId(), stackTraceToSingleLineString(e))); 1043 } 1044 } 1045 1046 /** 1047 * Terminate the append only mode of the database. 1048 * <p> 1049 * This should be called only when database was previously switched to append only mode. 1050 */ 1051 private void endAppendOnlyMode() throws DirectoryException 1052 { 1053 try 1054 { 1055 // FIXME: use full programmatic way of ending append mode once available in persistIt 1056 db.getManagement().execute("backup -e"); 1057 } 1058 catch (RemoteException e) 1059 { 1060 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1061 ERR_BACKEND_END_APPEND_MODE.get(config.getBackendId(), stackTraceToSingleLineString(e))); 1062 } 1063 } 1064 1065 @Override 1066 public void createBackup(BackupConfig backupConfig) throws DirectoryException 1067 { 1068 if (db != null) 1069 { 1070 switchToAppendOnlyMode(); 1071 } 1072 try 1073 { 1074 new BackupManager(config.getBackendId()).createBackup(this, backupConfig); 1075 } 1076 finally 1077 { 1078 if (db != null) 1079 { 1080 endAppendOnlyMode(); 1081 } 1082 } 1083 } 1084 1085 @Override 1086 public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException 1087 { 1088 new BackupManager(config.getBackendId()).removeBackup(backupDirectory, backupID); 1089 } 1090 1091 @Override 1092 public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException 1093 { 1094 new BackupManager(config.getBackendId()).restoreBackup(this, restoreConfig); 1095 } 1096 1097 @Override 1098 public Set<TreeName> listTrees() 1099 { 1100 try 1101 { 1102 String[] treeNames = volume.getTreeNames(); 1103 final Set<TreeName> results = new HashSet<>(treeNames.length); 1104 for (String treeName : treeNames) 1105 { 1106 results.add(TreeName.valueOf(treeName)); 1107 } 1108 return results; 1109 } 1110 catch (PersistitException e) 1111 { 1112 throw new StorageRuntimeException(e); 1113 } 1114 } 1115 1116 /** 1117 * TODO: it would be nice to use the low-level key/value APIs. They seem quite 1118 * inefficient at the moment for simple byte arrays. 1119 */ 1120 private static Key bytesToKey(final Key key, final ByteSequence bytes) 1121 { 1122 final byte[] tmp = bytes.toByteArray(); 1123 return key.clear().appendByteArray(tmp, 0, tmp.length); 1124 } 1125 1126 private static Value bytesToValue(final Value value, final ByteSequence bytes) 1127 { 1128 value.clear().putByteArray(bytes.toByteArray()); 1129 return value; 1130 } 1131 1132 private static ByteString valueToBytes(final Value value) 1133 { 1134 if (value.isDefined()) 1135 { 1136 return ByteString.wrap(value.getByteArray()); 1137 } 1138 return null; 1139 } 1140 1141 /** {@inheritDoc} */ 1142 @Override 1143 public boolean isConfigurationChangeAcceptable(PDBBackendCfg newCfg, 1144 List<LocalizableMessage> unacceptableReasons) 1145 { 1146 long newSize = computeSize(newCfg); 1147 long oldSize = computeSize(config); 1148 return (newSize <= oldSize || memQuota.isMemoryAvailable(newSize - oldSize)) 1149 && checkConfigurationDirectories(newCfg, unacceptableReasons); 1150 } 1151 1152 private long computeSize(PDBBackendCfg cfg) 1153 { 1154 return cfg.getDBCacheSize() > 0 ? cfg.getDBCacheSize() : memQuota.memPercentToBytes(cfg.getDBCachePercent()); 1155 } 1156 1157 /** 1158 * Checks newly created backend has a valid configuration. 1159 * @param cfg the new configuration 1160 * @param unacceptableReasons the list of accumulated errors and their messages 1161 * @param context TODO 1162 * @return true if newly created backend has a valid configuration 1163 */ 1164 static boolean isConfigurationAcceptable(PDBBackendCfg cfg, List<LocalizableMessage> unacceptableReasons, 1165 ServerContext context) 1166 { 1167 if (context != null) 1168 { 1169 MemoryQuota memQuota = context.getMemoryQuota(); 1170 if (cfg.getDBCacheSize() > 0 && !memQuota.isMemoryAvailable(cfg.getDBCacheSize())) 1171 { 1172 unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_SIZE_GREATER_THAN_JVM_HEAP.get( 1173 cfg.getDBCacheSize(), memQuota.getAvailableMemory())); 1174 return false; 1175 } 1176 else if (!memQuota.isMemoryAvailable(memQuota.memPercentToBytes(cfg.getDBCachePercent()))) 1177 { 1178 unacceptableReasons.add(ERR_BACKEND_CONFIG_CACHE_PERCENT_GREATER_THAN_JVM_HEAP.get( 1179 cfg.getDBCachePercent(), memQuota.memBytesToPercent(memQuota.getAvailableMemory()))); 1180 return false; 1181 } 1182 } 1183 return checkConfigurationDirectories(cfg, unacceptableReasons); 1184 } 1185 1186 private static boolean checkConfigurationDirectories(PDBBackendCfg cfg, 1187 List<LocalizableMessage> unacceptableReasons) 1188 { 1189 final ConfigChangeResult ccr = new ConfigChangeResult(); 1190 File parentDirectory = getFileForPath(cfg.getDBDirectory()); 1191 File newBackendDirectory = new File(parentDirectory, cfg.getBackendId()); 1192 1193 checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, true); 1194 checkDBDirPermissions(cfg, ccr); 1195 if (!ccr.getMessages().isEmpty()) 1196 { 1197 unacceptableReasons.addAll(ccr.getMessages()); 1198 return false; 1199 } 1200 return true; 1201 } 1202 1203 /** 1204 * Checks a directory exists or can actually be created. 1205 * 1206 * @param backendDir the directory to check for 1207 * @param ccr the list of reasons to return upstream or null if called from setupStorage() 1208 * @param cleanup true if the directory should be deleted after creation 1209 */ 1210 private static void checkDBDirExistsOrCanCreate(File backendDir, ConfigChangeResult ccr, boolean cleanup) 1211 { 1212 if (!backendDir.exists()) 1213 { 1214 if(!backendDir.mkdirs()) 1215 { 1216 addErrorMessage(ccr, ERR_CREATE_FAIL.get(backendDir.getPath())); 1217 } 1218 if (cleanup) 1219 { 1220 backendDir.delete(); 1221 } 1222 } 1223 else if (!backendDir.isDirectory()) 1224 { 1225 addErrorMessage(ccr, ERR_DIRECTORY_INVALID.get(backendDir.getPath())); 1226 } 1227 } 1228 1229 /** 1230 * Returns false if directory permissions in the configuration are invalid. Otherwise returns the 1231 * same value as it was passed in. 1232 * 1233 * @param cfg a (possibly new) backend configuration 1234 * @param ccr the current list of change results 1235 * @throws forwards a file exception 1236 */ 1237 private static void checkDBDirPermissions(PDBBackendCfg cfg, ConfigChangeResult ccr) 1238 { 1239 try 1240 { 1241 FilePermission backendPermission = decodeDBDirPermissions(cfg); 1242 // Make sure the mode will allow the server itself access to the database 1243 if(!backendPermission.isOwnerWritable() || 1244 !backendPermission.isOwnerReadable() || 1245 !backendPermission.isOwnerExecutable()) 1246 { 1247 addErrorMessage(ccr, ERR_CONFIG_BACKEND_INSANE_MODE.get(cfg.getDBDirectoryPermissions())); 1248 } 1249 } 1250 catch(ConfigException ce) 1251 { 1252 addErrorMessage(ccr, ce.getMessageObject()); 1253 } 1254 } 1255 1256 /** 1257 * Sets files permissions on the backend directory. 1258 * 1259 * @param backendDir the directory to setup 1260 * @param curCfg a backend configuration 1261 */ 1262 private void setDBDirPermissions(PDBBackendCfg curCfg, File backendDir) throws ConfigException 1263 { 1264 FilePermission backendPermission = decodeDBDirPermissions(curCfg); 1265 1266 // Get the backend database backendDirectory permissions and apply 1267 try 1268 { 1269 if(!FilePermission.setPermissions(backendDir, backendPermission)) 1270 { 1271 logger.warn(WARN_UNABLE_SET_PERMISSIONS, backendPermission, backendDir); 1272 } 1273 } 1274 catch(Exception e) 1275 { 1276 // Log an warning that the permissions were not set. 1277 logger.warn(WARN_SET_PERMISSIONS_FAILED, backendDir, e); 1278 } 1279 } 1280 1281 private static FilePermission decodeDBDirPermissions(PDBBackendCfg curCfg) throws ConfigException 1282 { 1283 try 1284 { 1285 return FilePermission.decodeUNIXMode(curCfg.getDBDirectoryPermissions()); 1286 } 1287 catch (Exception e) 1288 { 1289 throw new ConfigException(ERR_CONFIG_BACKEND_MODE_INVALID.get(curCfg.dn())); 1290 } 1291 } 1292 1293 /** {@inheritDoc} */ 1294 @Override 1295 public ConfigChangeResult applyConfigurationChange(PDBBackendCfg cfg) 1296 { 1297 final ConfigChangeResult ccr = new ConfigChangeResult(); 1298 1299 try 1300 { 1301 File parentDirectory = getFileForPath(cfg.getDBDirectory()); 1302 File newBackendDirectory = new File(parentDirectory, cfg.getBackendId()); 1303 1304 // Create the directory if it doesn't exist. 1305 if(!cfg.getDBDirectory().equals(config.getDBDirectory())) 1306 { 1307 checkDBDirExistsOrCanCreate(newBackendDirectory, ccr, false); 1308 if (!ccr.getMessages().isEmpty()) 1309 { 1310 return ccr; 1311 } 1312 1313 ccr.setAdminActionRequired(true); 1314 ccr.addMessage(NOTE_CONFIG_DB_DIR_REQUIRES_RESTART.get(config.getDBDirectory(), cfg.getDBDirectory())); 1315 } 1316 1317 if (!cfg.getDBDirectoryPermissions().equalsIgnoreCase(config.getDBDirectoryPermissions()) 1318 || !cfg.getDBDirectory().equals(config.getDBDirectory())) 1319 { 1320 checkDBDirPermissions(cfg, ccr); 1321 if (!ccr.getMessages().isEmpty()) 1322 { 1323 return ccr; 1324 } 1325 1326 setDBDirPermissions(cfg, newBackendDirectory); 1327 } 1328 diskMonitor.registerMonitoredDirectory( 1329 config.getBackendId() + " backend", 1330 getDirectory(), 1331 cfg.getDiskLowThreshold(), 1332 cfg.getDiskFullThreshold(), 1333 this); 1334 config = cfg; 1335 } 1336 catch (Exception e) 1337 { 1338 addErrorMessage(ccr, LocalizableMessage.raw(stackTraceToSingleLineString(e))); 1339 } 1340 return ccr; 1341 } 1342 1343 private static void addErrorMessage(final ConfigChangeResult ccr, LocalizableMessage message) 1344 { 1345 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 1346 ccr.addMessage(message); 1347 } 1348 1349 private void setupStorageFiles() throws ConfigException 1350 { 1351 ConfigChangeResult ccr = new ConfigChangeResult(); 1352 1353 checkDBDirExistsOrCanCreate(backendDirectory, ccr, false); 1354 if (!ccr.getMessages().isEmpty()) 1355 { 1356 throw new ConfigException(ccr.getMessages().get(0)); 1357 } 1358 checkDBDirPermissions(config, ccr); 1359 if (!ccr.getMessages().isEmpty()) 1360 { 1361 throw new ConfigException(ccr.getMessages().get(0)); 1362 } 1363 setDBDirPermissions(config, backendDirectory); 1364 } 1365 1366 @Override 1367 public void removeStorageFiles() throws StorageRuntimeException 1368 { 1369 if (!backendDirectory.exists()) 1370 { 1371 return; 1372 } 1373 1374 if (!backendDirectory.isDirectory()) 1375 { 1376 throw new StorageRuntimeException(ERR_DIRECTORY_INVALID.get(backendDirectory.getPath()).toString()); 1377 } 1378 1379 try 1380 { 1381 File[] files = backendDirectory.listFiles(); 1382 for (File f : files) 1383 { 1384 f.delete(); 1385 } 1386 } 1387 catch (Exception e) 1388 { 1389 logger.traceException(e); 1390 throw new StorageRuntimeException(ERR_REMOVE_FAIL.get(e.getMessage()).toString(), e); 1391 } 1392 1393 } 1394 1395 @Override 1396 public StorageStatus getStorageStatus() 1397 { 1398 return storageStatus; 1399 } 1400 1401 /** {@inheritDoc} */ 1402 @Override 1403 public void diskFullThresholdReached(File directory, long thresholdInBytes) { 1404 storageStatus = StorageStatus.unusable( 1405 WARN_DISK_SPACE_FULL_THRESHOLD_CROSSED.get(directory.getFreeSpace(), directory.getAbsolutePath(), 1406 thresholdInBytes, config.getBackendId())); 1407 } 1408 1409 /** {@inheritDoc} */ 1410 @Override 1411 public void diskLowThresholdReached(File directory, long thresholdInBytes) { 1412 storageStatus = StorageStatus.lockedDown( 1413 WARN_DISK_SPACE_LOW_THRESHOLD_CROSSED.get(directory.getFreeSpace(), directory.getAbsolutePath(), 1414 thresholdInBytes, config.getBackendId())); 1415 } 1416 1417 /** {@inheritDoc} */ 1418 @Override 1419 public void diskSpaceRestored(File directory, long lowThresholdInBytes, long fullThresholdInBytes) { 1420 storageStatus = StorageStatus.working(); 1421 } 1422} 1423