From c274af47a0d932b86f9afb86338ebff811edb4ee Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Fri, 05 Jun 2015 13:55:40 +0000
Subject: [PATCH] OPENDJ-1811 CR-7142 Disk space issues can corrupt file based changelog

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java |   90 +++++++++++++++++++++++++++++++++------------
 1 files changed, 66 insertions(+), 24 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
index 5a6c2aa..bc947e7 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
@@ -21,7 +21,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2014 ForgeRock AS
+ *      Copyright 2014-2015 ForgeRock AS
  */
 package org.opends.server.replication.server.changelog.file;
 
@@ -37,6 +37,10 @@
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -104,7 +108,7 @@
  * |   \---changenumberindex
  * |      \--- head.log [contains last records written]
  * |      \--- 1_50.log [contains records with keys in interval [1, 50]]
- * |      \--- rtime198745512.last
+ * |      \--- rotationtime198745512.last
  * |   \---1.domain
  * |       \---generation1.id
  * |       \---22.server
@@ -122,6 +126,7 @@
  */
 class ReplicationEnvironment
 {
+
   private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
   private static final long CN_INDEX_DB_MAX_LOG_FILE_SIZE_IN_BYTES = 1024 * 1024;
@@ -130,6 +135,9 @@
 
   private static final int NO_GENERATION_ID = -1;
 
+  /** Extension for the temporary file used when modifying an environment file. */
+  private static final String FILE_EXTENSION_TEMP = ".tmp";
+
   private static final String CN_INDEX_DB_DIRNAME = "changenumberindex";
 
   private static final String DOMAINS_STATE_FILENAME = "domains.state";
@@ -204,15 +212,14 @@
   private final ChangelogState changelogState;
 
   /** The list of logs that are in use for Replica DBs. */
-  private final List<Log<CSN, UpdateMsg>> logsReplicaDB = new CopyOnWriteArrayList<Log<CSN, UpdateMsg>>();
+  private final List<Log<CSN, UpdateMsg>> logsReplicaDB = new CopyOnWriteArrayList<>();
 
   /**
    * The list of logs that are in use for the CN Index DB.
    * There is a single CN Index DB for a ReplicationServer, but there can be multiple references opened on it.
-   * This is the responsability of Log class to handle properly these multiple references.
+   * This is the responsibility of Log class to handle properly these multiple references.
    */
-  private List<Log<Long, ChangeNumberIndexRecord>> logsCNIndexDB =
-      new CopyOnWriteArrayList<Log<Long, ChangeNumberIndexRecord>>();;
+  private List<Log<Long, ChangeNumberIndexRecord>> logsCNIndexDB = new CopyOnWriteArrayList<>();
 
   /**
    * Maps each domain DN to a domain id that is used to name directory in file system.
@@ -532,23 +539,19 @@
         return; // no serverId anymore => no-op
       }
       final File offlineFile = new File(serverIdPath, REPLICA_OFFLINE_STATE_FILENAME);
-      Writer writer = null;
-      try
+      try (Writer writer = newTempFileWriter(offlineFile))
       {
-        // Overwrite file, only the last sent offline CSN is kept
-        writer = newFileWriter(offlineFile);
+        // Only the last sent offline CSN is kept
         writer.write(offlineCSN.toString());
+        StaticUtils.close(writer);
         changelogState.addOfflineReplica(domainDN, offlineCSN);
+        commitFile(offlineFile);
       }
       catch (IOException e)
       {
         throw new ChangelogException(ERR_CHANGELOG_UNABLE_TO_WRITE_REPLICA_OFFLINE_STATE_FILE.get(
             domainDN.toString(), offlineCSN.getServerId(), offlineFile.getPath(), offlineCSN.toString()), e);
       }
-      finally
-      {
-        StaticUtils.close(writer);
-      }
     }
   }
 
@@ -715,24 +718,20 @@
     final String nextDomainId = findNextDomainId();
     domains.put(domainDN, nextDomainId);
     final File domainsStateFile = new File(replicationRootPath, DOMAINS_STATE_FILENAME);
-    Writer writer = null;
-    try
+    try (Writer writer = newTempFileWriter(domainsStateFile))
     {
-      writer = newFileWriter(domainsStateFile);
       for (final Entry<DN, String> entry : domains.entrySet())
       {
         writer.write(String.format("%s%s%s%n", entry.getValue(), DOMAIN_STATE_SEPARATOR, entry.getKey()));
       }
+      StaticUtils.close(writer);
+      commitFile(domainsStateFile);
     }
     catch (IOException e)
     {
       throw new ChangelogException(ERR_CHANGELOG_UNABLE_TO_UPDATE_DOMAIN_STATE_FILE.get(nextDomainId,
           domainDN.toString(), domainsStateFile.getPath()), e);
     }
-    finally
-    {
-      StaticUtils.close(writer);
-    }
     return nextDomainId;
   }
 
@@ -1016,10 +1015,53 @@
     }
   }
 
-  /** Returns a buffered writer on the provided file. */
-  private BufferedWriter newFileWriter(final File file) throws UnsupportedEncodingException, FileNotFoundException
+  /**
+   * Returns a buffered writer on the temp file (".tmp") corresponding to the provided file.
+   * <p>
+   * Once writes are finished, the {@code commitFile()} method should be called to finish the update
+   * of the provided file.
+   */
+  private BufferedWriter newTempFileWriter(final File file) throws UnsupportedEncodingException, FileNotFoundException
   {
-    return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), UTF8_ENCODING));
+    File tempFile = getTempFileFor(file);
+    return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tempFile), UTF8_ENCODING));
+  }
+
+  /**
+   * "Commit" the provided file by moving the ".tmp" file to its final location.
+   * <p>
+   * In order to prevent partially written environment files, update of files is always
+   * performed by writing first a ".tmp" version and then switching the ".tmp" version to
+   * the final version once update is finished.
+   * <p>
+   * This method effectively moves the ".tmp" version to the final version.
+   *
+   * @param file
+   *          the final file location.
+   */
+  private void commitFile(final File file) throws IOException
+  {
+    File tempFile = getTempFileFor(file);
+    try
+    {
+      Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE);
+    }
+    catch (FileAlreadyExistsException | AtomicMoveNotSupportedException e)
+    {
+      // The atomic move could fail depending on OS (mostly on old Windows versions)
+      // See OPENDJ-1811 for details
+      // Try to proceed with a non-atomic move
+      if (file.exists())
+      {
+        file.delete();
+      }
+      Files.move(tempFile.toPath(), file.toPath());
+    }
+  }
+
+  /** Returns a temporary file from provided file, by adding the ".tmp" suffix. */
+  private File getTempFileFor(File file) {
+    return new File(file.getParentFile(), file.getName() + FILE_EXTENSION_TEMP);
   }
 
   /** Returns a buffered reader on the provided file. */

--
Gitblit v1.10.0