summaryrefslogtreecommitdiff
path: root/qpid/java/bdbstore/src/main/java/org/apache/qpid/server/store/berkeleydb/BDBBackup.java
blob: 80802b6a0c4bfd6666a9e751830203df1766ce4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/*
 *
 * 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.qpid.server.store.berkeleydb;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.util.DbBackup;
import org.apache.log4j.Logger;

import org.apache.qpid.server.store.StoreException;
import org.apache.qpid.util.CommandLineParser;
import org.apache.qpid.util.FileUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

/**
 * BDBBackup is a utility for taking hot backups of the current state of a BDB transaction log database.
 *
 * <p/>This utility makes the following assumptions/performs the following actions:
 *
 * <p/><ul> <li>The from and to directory locations will already exist. This scripts does not create them. <li>If this
 * script fails to complete in one minute it will terminate. <li>This script always exits with code 1 on error, code 0
 * on success (standard unix convention). <li>This script will log out at info level, when it starts and ends and a list
 * of all files backed up. <li>This script logs all errors at error level. <li>This script does not perform regular
 * backups, wrap its calling script in a cron job or similar to do this. </ul>
 *
 * <p/>This utility is build around the BDB provided backup helper utility class, DbBackup. This utility class provides
 * an ability to force BDB to stop writing to the current log file set, whilst the backup is taken, to ensure that a
 * consistent snapshot is acquired. Preventing BDB from writing to the current log file set, does not stop BDB from
 * continuing to run concurrently while the backup is running, it simply moves onto a new set of log files; this
 * provides a 'hot' backup facility.
 *
 * <p/>DbBackup can also help with incremental backups, by providing the number of the last log file backed up.
 * Subsequent backups can be taken, from later log files only. In a messaging application, messages are not expected to
 * be long-lived in most cases, so the log files will usually have been completely turned over between backups. This
 * utility does not support incremental backups for this reason.
 *
 * <p/>If the database is locked by BDB, as is required when using transactions, and therefore will always be the case
 * in Qpid, this utility cannot make use of the DbBackup utility in a seperate process. DbBackup, needs to ensure that
 * the BDB envinronment used to take the backup has exclusive write access to the log files. This utility can take a
 * backup as a standalone utility against log files, when a broker is not running, using the {@link #takeBackup(String,
 *String,com.sleepycat.je.Environment)} method.
 *
 * <p/>A seperate backup machanism is provided by the {@link #takeBackupNoLock(String,String)} method which can take a
 * hot backup against a running broker. This works by finding out the set of files to copy, and then opening them all to
 * read, and repeating this process until a consistent set of open files is obtained. This is done to avoid the
 * situation where the BDB cleanup thread deletes a file, between the directory listing and opening of the file to copy.
 * All consistently opened files are copied. This is the default mechanism the the {@link #main} method of this utility
 * uses.
 *
 * <p/><table id="crc><caption>CRC Card</caption> <tr><th> Responsibilities <th> Collaborations <tr><td> Hot copy all
 * BDB log files from one directory to another. </table>
 */
public class BDBBackup
{
    /** Used for debugging. */
    private static final Logger log = Logger.getLogger(BDBBackup.class);

    /** Used for communicating with the user. */
    private static final Logger console = Logger.getLogger("Console");

    /** Defines the suffix used to identify BDB log files. */
    private static final String LOG_FILE_SUFFIX = ".jdb";

    /** Defines the command line format for this utility. */
    public static final String[][] COMMAND_LINE_SPEC =
        new String[][]
        {
            { "fromdir", "The path to the directory to back the bdb log file from.", "dir", "true" },
            { "todir", "The path to the directory to save the backed up bdb log files to.", "dir", "true" }
        };

    /** Defines the timeout to terminate the backup operation on if it fails to complete. One minte. */
    public static final long TIMEOUT = 60000;

    /**
     * Runs a backup of the BDB log files in a specified directory, copying the backed up files to another specified
     * directory.
     *
     * <p/>The following arguments must be specified:
     *
     * <p/><table><caption>Command Line</caption> <tr><th> Option <th> Comment <tr><td> -fromdir <td> The path to the
     * directory to back the bdb log file from. <tr><td> -todir   <td> The path to the directory to save the backed up
     * bdb log files to. </table>
     *
     * @param args The command line arguments.
     */
    public static void main(String[] args)
    {
        // Process the command line using standard handling (errors and usage followed by System.exit when it is wrong).
        Properties options =
            CommandLineParser.processCommandLine(args, new CommandLineParser(COMMAND_LINE_SPEC), System.getProperties());

        // Extract the from and to directory locations and perform a backup between them.
        try
        {
            String fromDir = options.getProperty("fromdir");
            String toDir = options.getProperty("todir");

            log.info("BDBBackup Utility: Starting Hot Backup.");

            BDBBackup bdbBackup = new BDBBackup();
            String[] backedUpFiles = bdbBackup.takeBackupNoLock(fromDir, toDir);

            if (log.isInfoEnabled())
            {
                log.info("BDBBackup Utility: Hot Backup Completed.");
                log.info(backedUpFiles.length + " file(s) backed-up:");
                for(String backedUpFile : backedUpFiles)
                {
                    log.info(backedUpFile);
                }
            }
        }
        catch (Exception e)
        {
            console.info("Backup script encountered an error and has failed: " + e.getMessage());
            log.error("Backup script got exception: " + e.getMessage(), e);
            System.exit(1);
        }
    }

    /**
     * Creates a backup of the BDB log files in the source directory, copying them to the destination directory.
     *
     * @param fromdir     The source directory path.
     * @param todir       The destination directory path.
     * @param environment An open BDB environment to perform the back up.
     *
     * @throws DatabaseException Any underlying execeptions from BDB are allowed to fall through.
     */
    public void takeBackup(String fromdir, String todir, Environment environment) throws DatabaseException
    {
        DbBackup backupHelper = null;

        try
        {
            backupHelper = new DbBackup(environment);

            // Prevent BDB from writing to its log files while the backup it taken.
            backupHelper.startBackup();

            // Back up the BDB log files to the destination directory.
            String[] filesForBackup = backupHelper.getLogFilesInBackupSet();

            for (int i = 0; i < filesForBackup.length; i++)
            {
                File sourceFile = new File(fromdir + File.separator + filesForBackup[i]);
                File destFile = new File(todir + File.separator + filesForBackup[i]);
                FileUtils.copy(sourceFile, destFile);
            }
        }
        finally
        {
            // Remember to exit backup mode, or all log files won't be cleaned and disk usage will bloat.
            if (backupHelper != null)
            {
                backupHelper.endBackup();
            }
        }
    }

    /**
     * Takes a hot backup when another process has locked the BDB database.
     *
     * @param fromdir The source directory path.
     * @param todir   The destination directory path.
     *
     * @return A list of all of the names of the files succesfully backed up.
     */
    public String[] takeBackupNoLock(String fromdir, String todir)
    {
        if (log.isDebugEnabled())
        {
            log.debug("public void takeBackupNoLock(String fromdir = " + fromdir + ", String todir = " + todir
                + "): called");
        }

        File fromDirFile = new File(fromdir);

        if (!fromDirFile.isDirectory())
        {
            throw new IllegalArgumentException("The specified fromdir(" + fromdir
                + ") must be the directory containing your bdbstore.");
        }

        File toDirFile = new File(todir);

        if (!toDirFile.exists())
        {
            // Create directory if it doesn't exist
            toDirFile.mkdirs();

            if (log.isDebugEnabled())
            {
                log.debug("Created backup directory:" + toDirFile);
            }
        }

        if (!toDirFile.isDirectory())
        {
            throw new IllegalArgumentException("The specified todir(" + todir + ") must be a directory.");
        }

        // Repeat until manage to open consistent set of files for reading.
        boolean consistentSet = false;
        FileInputStream[] fileInputStreams = new FileInputStream[0];
        File[] fileSet = new File[0];
        long start = System.currentTimeMillis();

        while (!consistentSet)
        {
            // List all .jdb files in the directory.
            fileSet = fromDirFile.listFiles(new FilenameFilter()
                    {
                        public boolean accept(File dir, String name)
                        {
                            return name.endsWith(LOG_FILE_SUFFIX);
                        }
                    });

            // Open them all for reading.
            fileInputStreams = new FileInputStream[fileSet.length];

            if (fileSet.length == 0)
            {
                throw new StoreException("There are no BDB log files to backup in the " + fromdir + " directory.");
            }

            for (int i = 0; i < fileSet.length; i++)
            {
                try
                {
                    fileInputStreams[i] = new FileInputStream(fileSet[i]);
                }
                catch (FileNotFoundException e)
                {
                    // Close any files opened for reading so far.
                    for (int j = 0; j < i; j++)
                    {
                        if (fileInputStreams[j] != null)
                        {
                            try
                            {
                                fileInputStreams[j].close();
                            }
                            catch (IOException ioEx)
                            {
                                // Rethrow this as a runtime exception, as something strange has happened.
                                throw new StoreException(ioEx);
                            }
                        }
                    }

                    // Could not open a consistent file set so try again.
                    break;
                }

                // A consistent set has been opened if all files were sucesfully opened for reading.
                if (i == (fileSet.length - 1))
                {
                    consistentSet = true;
                }
            }

            // Check that the script has not timed out, and raise an error if it has.
            long now = System.currentTimeMillis();
            if ((now - start) > TIMEOUT)
            {
                throw new StoreException("Hot backup script failed to complete in " + (TIMEOUT / 1000) + " seconds.");
            }
        }

        // Copy the consistent set of open files.
        List<String> backedUpFileNames = new LinkedList<String>();

        for (int j = 0; j < fileSet.length; j++)
        {
            File destFile = new File(todir + File.separator + fileSet[j].getName());
            try
            {
                FileUtils.copy(fileSet[j], destFile);
            }
            catch (RuntimeException re)
            {
                Throwable cause = re.getCause();
                if ((cause != null) && (cause instanceof IOException))
                {
                    throw new StoreException(re.getMessage() + " fromDir:" + fromdir + " toDir:" + toDirFile, cause);
                }
                else
                {
                    throw re;
                }
            }

            backedUpFileNames.add(destFile.getName());

            // Close all of the files.
            try
            {
                fileInputStreams[j].close();
            }
            catch (IOException e)
            {
                // Rethrow this as a runtime exception, as something strange has happened.
                throw new StoreException(e);
            }
        }

        return backedUpFileNames.toArray(new String[backedUpFileNames.size()]);
    }

    /*
     * Creates an environment for the bdb log files in the specified directory. This envrinonment can only be used
     * to backup these files, if they are not locked by another database instance.
     *
     * @param fromdir The path to the directory to create the environment for.
     *
     * @throws DatabaseException Any underlying exceptions from BDB are allowed to fall through.
     */
    private Environment createSourceDirEnvironment(String fromdir) throws DatabaseException
    {
        // Initialize the BDB backup utility on the source directory.
        return new Environment(new File(fromdir), new EnvironmentConfig());
    }
}