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