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