db-derby-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From krist...@apache.org
Subject svn commit: r1188109 - in /db/derby/code/trunk/java: engine/org/apache/derby/impl/services/monitor/ engine/org/apache/derby/loc/ shared/org/apache/derby/shared/common/reference/ testing/org/apache/derbyTesting/functionTests/tests/store/
Date Mon, 24 Oct 2011 12:18:20 GMT
Author: kristwaa
Date: Mon Oct 24 12:18:19 2011
New Revision: 1188109

URL: http://svn.apache.org/viewvc?rev=1188109&view=rev
Log:
DERBY-5283: Crash / process termination during SYSCS_DISABLE_LOG_ARCHIVE_MODE can leave service.properties
broken

Adds recover logic for service.properties. To be able to recover there must be
a backup file present. There are three different cases the logic can handle:
 o corrupt original (no EOF token) and backup: use backup
 o orignal and backup: delete backup
 o backup only: rename to original

Patch file: derby-5283-1b-recover.diff


Added:
    db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java
  (with props)
Modified:
    db/derby/code/trunk/java/engine/org/apache/derby/impl/services/monitor/StorageFactoryService.java
    db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml
    db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/MessageId.java
    db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
    db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/_Suite.java

Modified: db/derby/code/trunk/java/engine/org/apache/derby/impl/services/monitor/StorageFactoryService.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/impl/services/monitor/StorageFactoryService.java?rev=1188109&r1=1188108&r2=1188109&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/impl/services/monitor/StorageFactoryService.java
(original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/impl/services/monitor/StorageFactoryService.java
Mon Oct 24 12:18:19 2011
@@ -39,14 +39,18 @@ import org.apache.derby.io.WritableStora
 import org.apache.derby.iapi.reference.Attribute;
 import org.apache.derby.iapi.reference.Property;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.FileReader;
 import java.io.InputStream;
 import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
 import java.io.OutputStream;
 import java.io.IOException;
 import java.io.FileNotFoundException;
+import java.io.OutputStreamWriter;
 
 import java.util.Enumeration;
 import java.util.NoSuchElementException;
@@ -64,6 +68,9 @@ import org.apache.derby.iapi.services.io
  */
 final class StorageFactoryService implements PersistentService
 {
+    /** Marker printed as the last line of the service properties file. */
+    private static final String SERVICE_PROPERTIES_EOF_TOKEN =
+            "#--- last line, don't put anything after this line ---";
 
     private String home; // the path of the database home directory. Can be null
     private String canonicalHome; // will be null if home is null
@@ -276,6 +283,7 @@ final class StorageFactoryService implem
                         {
                             StorageFactory storageFactory = privGetStorageFactoryInstance(
true, serviceName, null, null);
                             StorageFile file = storageFactory.newStorageFile( PersistentService.PROPERTIES_NAME);
+                            resolveServicePropertiesFiles(storageFactory, file);
                             try {
                                 InputStream is = file.getInputStream();
                                 try {
@@ -323,6 +331,7 @@ final class StorageFactoryService implem
         if( ! (sf instanceof WritableStorageFactory))
             throw StandardException.newException(SQLState.READ_ONLY_SERVICE);
         final WritableStorageFactory storageFactory = (WritableStorageFactory) sf;
+        // Write the service properties to file.
         try
         {
             AccessController.doPrivileged(
@@ -330,63 +339,79 @@ final class StorageFactoryService implem
                 {
                     public Object run() throws StandardException
                     {
-                        StorageFile backupFile = null;
+                        StorageFile backupFile = replace
+                            ? storageFactory.newStorageFile(
+                                PersistentService.PROPERTIES_NAME.concat("old"))
+                            : null;
                         StorageFile servicePropertiesFile = storageFactory.newStorageFile(
PersistentService.PROPERTIES_NAME);
+                        FileOperationHelper foh = new FileOperationHelper();
 
                         if (replace)
                         {
-                            backupFile = storageFactory.newStorageFile( PersistentService.PROPERTIES_NAME.concat("old"));
-                            try
-                            {
-                                if(!servicePropertiesFile.renameTo(backupFile))
-                                    throw StandardException.newException(SQLState.UNABLE_TO_RENAME_FILE,
-                                                                         servicePropertiesFile,
backupFile);
-                            }
-                            catch (SecurityException se) { throw Monitor.exceptionStartingModule(se);
}
+                            foh.renameTo(
+                                    servicePropertiesFile, backupFile, true);
                         }
 
                         OutputStream os = null;
                         try
                         {
                             os = servicePropertiesFile.getOutputStream();
-                            properties.store( os, serviceName + MessageService.getTextMessage(MessageId.SERVICE_PROPERTIES_DONT_EDIT));
+                            properties.store(os, serviceName +
+                                MessageService.getTextMessage(
+                                    MessageId.SERVICE_PROPERTIES_DONT_EDIT));
+                            BufferedWriter bOut = new BufferedWriter(
+                                    new OutputStreamWriter(os));
+                            bOut.write(SERVICE_PROPERTIES_EOF_TOKEN);
+                            bOut.newLine();
                             storageFactory.sync( os, false);
+                            bOut.close();
                             os.close();
-                            os = null;
+                            os = null; 
                         }
                         catch (IOException ioe)
                         {
+                            if (backupFile != null)
+                            {
+                                // Rename the old properties file back again.
+                                foh.renameTo(backupFile, servicePropertiesFile,
+                                        false);
+                            }
+                            if (replace)
+                            {
+                                throw StandardException.newException(
+                                        SQLState.SERVICE_PROPERTIES_EDIT_FAILED,
+                                        ioe);
+                            }
+                            else
+                            {
+                                throw Monitor.exceptionStartingModule(ioe);
+                            }
+                        }
+                        finally
+                        {
                             if (os != null)
                             {
                                 try
                                 {
                                     os.close();
                                 }
-                                catch (IOException ioe2) {}
-                                os = null;
-                            }
-
-                            if (backupFile != null)
-                            {
-                                // need to re-name the old properties file back again
-                                try
+                                catch (IOException ioe)
                                 {
-                                    servicePropertiesFile.delete();
-                                    backupFile.renameTo(servicePropertiesFile);
+                                    // Ignore exception on close
                                 }
-                                catch (SecurityException se) {}
                             }
-                            throw Monitor.exceptionStartingModule(ioe);
                         }
 		
                         if (backupFile != null)
                         {
-                            try
+                            if (!foh.delete(backupFile, false))
                             {
-                                backupFile.delete();
-                                backupFile = null;
+                                Monitor.getStream().printlnWithHeader(
+                                    MessageService.getTextMessage(
+                                        MessageId.SERVICE_PROPERTIES_BACKUP_DEL_FAILED,
+                                        getMostAccuratePath(backupFile)));
+                                
                             }
-                            catch (SecurityException se) {}
                         }
                         return null;
                     }
@@ -454,6 +479,91 @@ final class StorageFactoryService implem
                 );
         }catch( PrivilegedActionException pae) { throw (StandardException) pae.getException();}
     }
+    
+    /**
+     * Resolves situations where a failure condition left the service properties
+     * file, and/or the service properties file backup, in an inconsistent
+     * state.
+     * <p>
+     * Note that this method doesn't resolve the situation where both the
+     * current service properties file and the backup file are missing.
+     *
+     * @param sf the storage factory for the service
+     * @param spf the service properties file
+     * @throws StandardException if a file operation on a service properties
+     *      file fails
+     */
+    private void resolveServicePropertiesFiles(StorageFactory sf,
+                                               StorageFile spf)
+            throws StandardException {
+        StorageFile spfOld = sf.newStorageFile(PROPERTIES_NAME.concat("old"));
+        FileOperationHelper foh = new FileOperationHelper();
+        boolean hasCurrent = foh.exists(spf, true);
+        boolean hasBackup = foh.exists(spfOld, true);
+        // Shortcut the normal case.
+        if (hasCurrent && !hasBackup) {
+            return;
+        }
+
+        // Backup file, but no current file.
+        if (hasBackup && !hasCurrent) {
+            // Writing the new service properties file must have failed during
+            // an update. Rename the backup to be the current file.
+            foh.renameTo(spfOld, spf, true);
+            Monitor.getStream().printlnWithHeader(
+                                MessageService.getTextMessage(
+                                    MessageId.SERVICE_PROPERTIES_RESTORED));
+        // Backup file and current file.
+        } else if (hasBackup && hasCurrent) {
+            // See if the new (current) file is valid. If so delete the backup,
+            // if not, rename the backup to be the current.
+            BufferedReader bin = null;
+            String lastLine = null;
+            try {
+                bin = new BufferedReader(new FileReader(spf.getPath()));
+                String line;
+                while ((line = bin.readLine()) != null) {
+                    if (line.trim().length() != 0) {
+                        lastLine = line;
+                    }
+                }
+            } catch (IOException ioe) {
+                throw StandardException.newException(
+                        SQLState.UNABLE_TO_OPEN_FILE, ioe,
+                        spf.getPath(), ioe.getMessage());
+            } finally {
+                try {
+                    if (bin != null) {
+                        bin.close();
+                    }
+                } catch (IOException ioe) {
+                    // Ignore exception during close
+                }
+            }
+            if (lastLine != null &&
+                    lastLine.startsWith(SERVICE_PROPERTIES_EOF_TOKEN)) {
+                // Being unable to delete the backup file is fine as long as
+                // the current file appears valid.
+                String msg;
+                if (foh.delete(spfOld, false)) {
+                    msg = MessageService.getTextMessage(
+                            MessageId.SERVICE_PROPERTIES_BACKUP_DELETED);    
+                } else {
+                    // Include path so the user can delete file manually.
+                    msg = MessageService.getTextMessage(
+                            MessageId.SERVICE_PROPERTIES_BACKUP_DEL_FAILED,
+                            getMostAccuratePath(spfOld));
+                }
+                Monitor.getStream().printlnWithHeader(msg);
+            } else {
+                foh.delete(spf, false);
+                foh.renameTo(spfOld, spf, true);
+                Monitor.getStream().printlnWithHeader(
+                                MessageService.getTextMessage(
+                                    MessageId.SERVICE_PROPERTIES_RESTORED));
+            }
+        } 
+    }
                 
     /*
 	**Recreates service root if required depending on which of the following
@@ -691,7 +801,7 @@ final class StorageFactoryService implem
         if ( !service_properties.exists() )
         {
             throw StandardException.newException
-                ( SQLState.MISSING_SERVICE_PROPERTIES, serviceName, PersistentService.PROPERTIES_NAME
);
+                ( SQLState.SERVICE_PROPERTIES_MISSING, serviceName, PersistentService.PROPERTIES_NAME
);
         }
     }
 
@@ -956,4 +1066,114 @@ final class StorageFactoryService implem
             return null;
         } // end of run
     } // end of class DirectoryList
+
+    /**
+     * Helper method returning the "best-effort-most-accurate" path.
+     *
+     * @param file the file to get the path to
+     * @return The file path, either ala {@code File.getCanonicalPath} or
+     *      {@code File.getPath}.
+     */
+    private static String getMostAccuratePath(StorageFile file) {
+        String path = file.getPath();
+        try {
+            path = file.getCanonicalPath();
+        } catch (IOException ioe) {
+            // Ignore this, use path from above.
+        }
+        return path;
     }
+
+    /**
+     * Helper class for common file operations on the service properties files.
+     * <p>
+     * Introduced to take care of error reporting for common file operations
+     * carried out in StorageFactoryService.
+     */
+    //@NotThreadSafe
+    private static class FileOperationHelper {
+        /** Name of the most recently performed operation. */
+        private String operation;
+
+        boolean exists(StorageFile file, boolean mustSucceed)
+                throws StandardException {
+            operation = "exists";
+            boolean ret = false;
+            try {
+                ret = file.exists();
+            } catch (SecurityException se) {
+                handleSecPrivException(file, mustSucceed, se);
+            }
+            return ret;
+        }
+
+        boolean delete(StorageFile file, boolean mustSucceed)
+                throws StandardException {
+            operation = "delete";
+            boolean deleted = false;
+            try {
+                deleted = file.delete();
+            } catch (SecurityException se) {
+                handleSecPrivException(file, mustSucceed, se);
+            }
+            if (mustSucceed && !deleted) {
+                throw StandardException.newException(
+                        SQLState.UNABLE_TO_DELETE_FILE, file.getPath());   
+            }
+            return deleted;
+        }
+
+        boolean renameTo(StorageFile from, StorageFile to, boolean mustSucceed)
+                throws StandardException {
+            operation = "renameTo";
+            // Even if the explicit delete fails, the rename may succeed.
+            delete(to, false);
+            boolean renamed = false;
+            try {
+                renamed = from.renameTo(to);
+            } catch (SecurityException se) {
+                StorageFile file = to;
+                try {
+                    // We got a security exception, assume a secman is present.
+                    System.getSecurityManager().checkWrite(from.getPath());
+                } catch (SecurityException se1) {
+                    file = from;
+                }
+                handleSecPrivException(file, mustSucceed, se);
+            }
+            if (mustSucceed && !renamed) {
+                throw StandardException.newException(
+                        SQLState.UNABLE_TO_RENAME_FILE,
+                        from.getPath(), to.getPath());
+            }
+            return renamed;
+        }
+        
+        /**
+         * Handles security exceptions caused by missing privileges on the
+         * files being accessed.
+         *
+         * @param file the file that was accessed
+         * @param mustSucceed if {@code true} a {@code StandardException} will
+         *      be thrown, if {@code false} a warning is written to the log
+         * @param se the security exception raised
+         * @throws StandardException if {@code mustSucceed} is {@code true}
+         * @throws NullPointerException if {@code file} or {@code se} is null
+         */
+        private void handleSecPrivException(StorageFile file,
+                                            boolean mustSucceed,
+                                            SecurityException se)
+                throws StandardException {
+            if (mustSucceed) {
+                throw StandardException.newException(
+                        SQLState.MISSING_FILE_PRIVILEGE, se, operation,
+                        file.getName(), se.getMessage());
+            } else {
+                Monitor.getStream().printlnWithHeader(
+                        MessageService.getTextMessage(
+                        SQLState.MISSING_FILE_PRIVILEGE, operation,
+                        getMostAccuratePath(file), se.getMessage())); 
+            }
+        }
+    } // End of static class FileOperationHelper
+}

Modified: db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml?rev=1188109&r1=1188108&r2=1188109&view=diff
==============================================================================
--- db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml (original)
+++ db/derby/code/trunk/java/engine/org/apache/derby/loc/messages.xml Mon Oct 24 12:18:19
2011
@@ -3724,6 +3724,20 @@ Guide.
             </msg>
 
             <msg>
+                <name>XBM0B.D</name>
+                <text>Failed to edit/write service properties file: {0}</text>
+                <arg>errorMessage</arg>
+            </msg>
+
+            <msg>
+                <name>XBM0C.D</name>
+                <text>Missing privilege for operation '{0}' on file '{1}': {2}</text>
+                <arg>operation</arg>
+                <arg>path</arg>
+                <arg>errorMessage</arg>
+            </msg>
+
+            <msg>
                 <name>XBM0G.D</name>
                 <text>Failed to start encryption engine. Please make sure you are running
Java 2 and have downloaded an encryption provider such as jce and put it in your class path.
</text>
             </msg>
@@ -8194,6 +8208,22 @@ Shutting down instance {0} on database d
             </msg>
 
             <msg>
+                <name>M002</name>
+                <text>Restored missing/corrupted service properties from backup file.</text>
+            </msg>
+
+            <msg>
+                <name>M003</name>
+                <text>Deleted leftover service properties backup file.</text>
+            </msg>
+
+            <msg>
+                <name>M004</name>
+                <text>Failed to delete leftover service properties backup file, delete
it manually: {0}</text>
+                <arg>servicePropertiesBackupFile</arg>
+            </msg>
+
+            <msg>
                 <name>N001</name>
                 <text>caused by</text>
             </msg>

Modified: db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/MessageId.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/MessageId.java?rev=1188109&r1=1188108&r2=1188109&view=diff
==============================================================================
--- db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/MessageId.java
(original)
+++ db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/MessageId.java
Mon Oct 24 12:18:19 2011
@@ -205,7 +205,15 @@ public interface MessageId {
     /*
      * Monitor
      */
-    String SERVICE_PROPERTIES_DONT_EDIT = "M001"; // Tell user not to edit service.properties
+
+    /** Tell user not to edit service.properties. */
+    String SERVICE_PROPERTIES_DONT_EDIT                     = "M001";
+    /** Informational message, service properties restored. */
+    String SERVICE_PROPERTIES_RESTORED                      = "M002";
+    /** Informational message, service properties backup deleted. */
+    String SERVICE_PROPERTIES_BACKUP_DELETED                = "M003";
+    /** Informational message, service properties backup deletion failed. */
+    String SERVICE_PROPERTIES_BACKUP_DEL_FAILED             = "M004";
 
     /*
      * Misc

Modified: db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java?rev=1188109&r1=1188108&r2=1188109&view=diff
==============================================================================
--- db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
(original)
+++ db/derby/code/trunk/java/shared/org/apache/derby/shared/common/reference/SQLState.java
Mon Oct 24 12:18:19 2011
@@ -164,7 +164,9 @@ public interface SQLState {
 	String SERVICE_MISSING_IMPLEMENTATION		= "XBM02.D";
 	String MISSING_PRODUCT_VERSION				= "XBM05.D";
 	String SERVICE_WRONG_BOOT_PASSWORD			= "XBM06.D";
-	String MISSING_SERVICE_PROPERTIES			= "XBM0A.D";
+    String SERVICE_PROPERTIES_MISSING			= "XBM0A.D";
+    String SERVICE_PROPERTIES_EDIT_FAILED       = "XBM0B.D";
+    String MISSING_FILE_PRIVILEGE               = "XBM0C.D";
 	String SERVICE_BOOT_PASSWORD_TOO_SHORT		= "XBM07.D";
 	String MISSING_ENCRYPTION_PROVIDER			= "XBM0G.D";
 	String SERVICE_DIRECTORY_CREATE_ERROR		= "XBM0H.D";

Added: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java?rev=1188109&view=auto
==============================================================================
--- db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java
(added)
+++ db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java
Mon Oct 24 12:18:19 2011
@@ -0,0 +1,375 @@
+/*
+
+   Derby - Class org.apache.derbyTesting.functionTests.tests.store.ServicePropertiesFileTest
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to you under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+ */
+
+package org.apache.derbyTesting.functionTests.tests.store;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import javax.sql.DataSource;
+
+import junit.framework.Test;
+
+import org.apache.derbyTesting.functionTests.util.PrivilegedFileOpsForTests;
+import org.apache.derbyTesting.junit.BaseJDBCTestCase;
+import org.apache.derbyTesting.junit.JDBCDataSource;
+import org.apache.derbyTesting.junit.TestConfiguration;
+
+/**
+ * Tests Derby's ability to recover from various conditions related to the
+ * service properties file.
+ * <p>
+ * The basic pattern of the tests is to start with a pristine database, modify
+ * 'service.properties' and/or 'service.propertiesold', and then finally boot
+ * the database and assert what happened with the two aforementioned files.
+ */
+public class ServicePropertiesFileTest
+        extends BaseJDBCTestCase {
+    
+    private static final String LOG_A_MODE =
+            "derby.storage.logArchiveMode";
+    /**
+     * End-of-file token used by Derby in 'service.properties'.
+     */
+    private static final String END_TOKEN =
+            "#--- last line, don't put anything after this line ---";
+    /** Logical name of the pristine database. */
+    private static final String DB_NAME = "spfTestDb";
+
+    /** Where the databases are living. */
+    private static File databasesDir;
+    /** Path to the pristine database. */
+    private static File pristineDb;
+    /** Whether the pristine database has been initialized or not. */
+    private static boolean dbInitialized;
+
+    /** Database that will be deleted during {@code shutDown}. */
+    private File dbToDelete;
+    /**
+     * Path to 'service.properties' of the current database.
+     * @see #copyDbAs
+     */
+    private File spf;
+    /**
+     * Path to 'service.propertiesold' of the current database.
+     * @see #copyDbAs
+     */
+    private File spfOld;
+
+    public ServicePropertiesFileTest(String name) {
+        super(name);
+    }
+
+    /**
+     * Initializes the pristine database if required.
+     */
+    public void setUp()
+            throws SQLException {
+        if (!dbInitialized) {
+            DataSource ds = JDBCDataSource.getDataSourceLogical(DB_NAME);
+            JDBCDataSource.setBeanProperty(ds, "createDatabase", "create");
+            ds.getConnection();
+            JDBCDataSource.shutdownDatabase(ds);
+            File systemHome = new File(getSystemProperty("derby.system.home"));
+            databasesDir = new File(systemHome, "singleUse");
+            pristineDb = new File(
+                    systemHome,
+                    TestConfiguration.getCurrent().getPhysicalDatabaseName(
+                        DB_NAME));
+            dbInitialized = true;
+        }        
+    }
+
+    /**
+     * Deletes the last database copy (if one exists).
+     */
+    public void tearDown()
+            throws Exception {
+        if (dbToDelete != null) {
+            assertDirectoryDeleted(dbToDelete);    
+        }
+        super.tearDown();
+    }
+
+    /**
+     * Tests what happens when the service properties file is missing and there
+     * is no backup available.
+     */
+    public void testMissingServicePropertiesFileNoBackup()
+            throws IOException, SQLException {
+        // Prepare
+        String db = "spfTestMissingSPFNB";
+        copyDbAs(db);
+        PrivilegedFileOpsForTests.delete(spf);
+        assertPresence(false, false);
+
+        // This will currently fail with a message saying the database wasn't
+        // found, even though everything is there except for the service
+        // properties file.
+        try {
+            connectThenShutdown(db);
+            fail("booted database without a service.properties file");
+        } catch (SQLException sqle) {
+            assertSQLState("error message has changed", "XJ004", sqle);
+        }
+    }
+
+    /**
+     * Tests handling of the situation where the service properties file is
+     * missing, but a backup is available.
+     * <p>
+     * The expected behavior is to restore (by renaming) the service properties
+     * file from the backup.
+     */
+    public void testMissingServicePropertiesFileWithBackup()
+            throws IOException, SQLException {
+        // Prepare
+        String db = "spfTestMissingSPFWB";
+        copyDbAs(db);
+        createSPFBackup(false);
+        assertPresence(false, true);
+
+        // Recover and assert
+        connectThenShutdown(db);
+        assertNormalPresence();
+    }
+
+    /**
+     * Tests handling of the situation where both the service properties file
+     * and the backup are available.
+     * <p>
+     * Expected behavior here is to delete the backup (given that the original
+     * service properties file contains the end-of-file token).
+     */
+    public void testSevicePropertiesFileWithBackup()
+            throws IOException, SQLException {
+        // Prepare
+        String db = "spfTestSPFWB";
+        copyDbAs(db);
+        createSPFBackup(true);
+        assertPresence(true, true);
+
+        // Recover and assert
+        connectThenShutdown(db);
+        assertNormalPresence();
+        assertEOFToken(spf);
+    }
+
+    /**
+     * Tests the situation where both the service properties file and a backup
+     * are available, but the service properties file is corrupted (see note
+     * below).
+     * <p>
+     * The expected behavior is to delete the original service properties file
+     * and then restore it from the backup (i.e. by renaming).
+     * <p>
+     * In this regard, a corrupt service properties file is one where the
+     * end-of-file token is missing. No other error conditions are detected,
+     * i.e. if properties are removed manually or the values are modified.
+     */
+    public void testSevicePropertiesFileCorruptedWithBackup()
+            throws IOException, SQLException {
+        // Prepare
+        String db = "spfTestSPFCWB";
+        copyDbAs(db);
+        createSPFBackup(true);
+        removeEOFToken(spf);
+        assertPresence(true, true);
+
+        // Recover and assert
+        connectThenShutdown(db);
+        assertNormalPresence();
+        assertEOFToken(spf);
+    }
+
+    /**
+     * Ensures that Derby can handle the case where the backup file already
+     * exists when editing the service properties.
+     */
+    public void testBackupWithBackupExisting()
+            throws IOException, SQLException {
+        // Prepare
+        String db = "spfTestBWBE";
+        copyDbAs(db);
+        assertPresence(true, false);
+        // Make sure 'derby.storage.logArchiveMode' isn't present already.
+        assertEquals(0, grepForToken(LOG_A_MODE, spf));
+
+        // Connect, then enable log archive mode to trigger edit.
+        DataSource ds = JDBCDataSource.getDataSource();
+        JDBCDataSource.setBeanProperty(ds, "databaseName", "singleUse/" + db);
+        Connection con = ds.getConnection();
+        // Create the service properties file backup.
+        createSPFBackup(true);
+
+        // Trigger service properties file edit.
+        Statement stmt = con.createStatement();
+        stmt.execute("CALL SYSCS_UTIL.SYSCS_DISABLE_LOG_ARCHIVE_MODE(0)");
+        con.close();
+        // Shut down the database.
+        JDBCDataSource.shutdownDatabase(ds);
+
+        assertNormalPresence();
+        assertEquals(1, grepForToken(LOG_A_MODE + "=false", spf));
+    }
+
+    /**
+     * Asserts that the presence of the service properties file and the backup
+     * is normal, that is that the former is present and the latter isn't.
+     */
+    private void assertNormalPresence() {
+        assertPresence(true, false);
+    }
+
+    /**
+     * Asserts the specified presence of the original and the backup service
+     * properties files.
+     *
+     * @param spfPresence presence of the original file
+     * @param spfOldPresence presence of the backup file
+     */
+    private void assertPresence(boolean spfPresence, boolean spfOldPresence) {
+        assertEquals("incorrect '" + spf.getAbsolutePath() + "' presence,",
+                spfPresence, PrivilegedFileOpsForTests.exists(spf));
+        assertEquals("incorrect '" + spfOld.getPath() + "' presence,",
+                spfOldPresence, PrivilegedFileOpsForTests.exists(spfOld));
+    }
+
+    /**
+     * Asserts that the specified file ends with the end-of-file token.
+     */
+    private void assertEOFToken(File file)
+            throws IOException {
+        BufferedReader in = new BufferedReader(
+                PrivilegedFileOpsForTests.getFileReader(file));
+        String prev = null;
+        String cur;
+        while ((cur = in.readLine()) != null) {
+            prev = cur;
+        }
+        in.close();
+        assertNotNull("last line is null - empty file?", prev);
+        assertTrue(prev.startsWith(END_TOKEN));
+    }
+
+    /**
+     * Removes the end-of-file token from the specified file.
+     */
+    private void removeEOFToken(File original)
+            throws IOException {
+        // Move file, then rewrite by removing last line (the token).
+        File renamed = new File(original.getAbsolutePath() + "-renamed");
+        PrivilegedFileOpsForTests.copy(original, renamed);
+        PrivilegedFileOpsForTests.delete(original);
+        BufferedReader in = new BufferedReader(
+                PrivilegedFileOpsForTests.getFileReader(renamed));
+        // Default charset should be 8859_1.
+        BufferedWriter out = new BufferedWriter(
+                PrivilegedFileOpsForTests.getFileWriter(original));
+        String prev = null;
+        String line;
+        while ((line = in.readLine()) != null) {
+            if (prev != null) {
+                out.write(prev);
+                out.newLine();
+            }
+            prev = line;
+        }
+        assertEquals(END_TOKEN, prev);
+        in.close();
+        out.close();
+        PrivilegedFileOpsForTests.delete(renamed);
+    }
+
+    /**
+     * Looks for the specified token in the given file.
+     *
+     * @param token the search token
+     * @param file the file to search
+     * @return The number of matching lines.
+     *
+     * @throws IOException if accessing the specified file fails
+     */
+    private int grepForToken(String token, File file)
+            throws IOException {
+        int matchingLines = 0;
+        BufferedReader in = new BufferedReader(
+                PrivilegedFileOpsForTests.getFileReader(file));    
+        String line;
+        while ((line = in.readLine()) != null) {
+            if (line.indexOf(token) != -1) {
+                matchingLines++;
+            }
+        }
+        in.close();
+        return matchingLines;
+    }
+            
+    /**
+     * Copies the master/pristine database to a new database.
+     *
+     * @param name name of the database to copy to
+     */
+    private void copyDbAs(String name)
+            throws IOException {
+        File newDb = new File(databasesDir, name);
+        dbToDelete = newDb;
+        PrivilegedFileOpsForTests.copy(pristineDb, newDb);
+        spf = new File(newDb, "service.properties");
+        spfOld = new File(newDb, "service.propertiesold");
+    }
+
+    /** Dependent on state set by {@linkplain copyDb}. */
+    private void createSPFBackup(boolean keepOriginal)
+            throws IOException {
+        PrivilegedFileOpsForTests.copy(spf, spfOld);
+        if (!keepOriginal) {
+            PrivilegedFileOpsForTests.delete(spf);
+        }
+    }
+
+    /**
+     * Connects to the specified database, then shuts it down.
+     * <p>
+     * This method is used to trigger the recovery logic for the service
+     * properties file.
+     *
+     * @param db database to connect to (expected to live in 'system/singleUse')
+     */ 
+    private void connectThenShutdown(String db)
+            throws SQLException {
+        DataSource ds = JDBCDataSource.getDataSource();
+        JDBCDataSource.setBeanProperty(ds, "databaseName", "singleUse/" + db);
+        ds.getConnection().close();
+        JDBCDataSource.shutdownDatabase(ds);
+    }
+
+    public static Test suite() {
+        return TestConfiguration.additionalDatabaseDecoratorNoShutdown(
+            TestConfiguration.embeddedSuite(ServicePropertiesFileTest.class),
+            DB_NAME);
+    }
+}

Propchange: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/ServicePropertiesFileTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/_Suite.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/_Suite.java?rev=1188109&r1=1188108&r2=1188109&view=diff
==============================================================================
--- db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/_Suite.java
(original)
+++ db/derby/code/trunk/java/testing/org/apache/derbyTesting/functionTests/tests/store/_Suite.java
Mon Oct 24 12:18:19 2011
@@ -78,6 +78,7 @@ public class _Suite extends BaseTestCase
         suite.addTest(RowLockBasicTest.suite());
         suite.addTest(RecoveryTest.suite());
         suite.addTest(TableLockBasicTest.suite());
+        suite.addTest(ServicePropertiesFileTest.suite());
 
         /* Tests that only run in sane builds */
         if (SanityManager.DEBUG) {



Mime
View raw message