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}