summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMark Benvenuto <mark.benvenuto@mongodb.com>2015-08-17 11:53:38 -0400
committerMark Benvenuto <mark.benvenuto@mongodb.com>2015-09-08 21:01:41 -0400
commit9271c08121a0f511b3326a8a2eca449a6a523309 (patch)
tree03f7362598c395dd64eb4c8a6b0ac59bf19f51d4 /src
parentd608d9b34814a82a6d512a5817160a099bfed90f (diff)
downloadmongo-9271c08121a0f511b3326a8a2eca449a6a523309.tar.gz
SERVER-19584: Implement full-time diagnostic data capture file reader and writer
Diffstat (limited to 'src')
-rw-r--r--src/mongo/db/ftdc/SConscript5
-rw-r--r--src/mongo/db/ftdc/file_manager.cpp301
-rw-r--r--src/mongo/db/ftdc/file_manager.h147
-rw-r--r--src/mongo/db/ftdc/file_manager_test.cpp320
-rw-r--r--src/mongo/db/ftdc/file_reader.cpp216
-rw-r--r--src/mongo/db/ftdc/file_reader.h144
-rw-r--r--src/mongo/db/ftdc/file_writer.cpp210
-rw-r--r--src/mongo/db/ftdc/file_writer.h144
-rw-r--r--src/mongo/db/ftdc/file_writer_test.cpp265
9 files changed, 1752 insertions, 0 deletions
diff --git a/src/mongo/db/ftdc/SConscript b/src/mongo/db/ftdc/SConscript
index b050d88f0bb..0b2a555dbd5 100644
--- a/src/mongo/db/ftdc/SConscript
+++ b/src/mongo/db/ftdc/SConscript
@@ -10,6 +10,9 @@ ftdcEnv.Library(
'block_compressor.cpp',
'compressor.cpp',
'decompressor.cpp',
+ 'file_manager.cpp',
+ 'file_reader.cpp',
+ 'file_writer.cpp',
'util.cpp',
'varint.cpp'
],
@@ -25,6 +28,8 @@ env.CppUnitTest(
target='ftdc_test',
source=[
'compressor_test.cpp',
+ 'file_manager_test.cpp',
+ 'file_writer_test.cpp',
'ftdc_test.cpp',
'util_test.cpp',
'varint_test.cpp',
diff --git a/src/mongo/db/ftdc/file_manager.cpp b/src/mongo/db/ftdc/file_manager.cpp
new file mode 100644
index 00000000000..94f5076ab82
--- /dev/null
+++ b/src/mongo/db/ftdc/file_manager.cpp
@@ -0,0 +1,301 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kFTDC
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/ftdc/file_manager.h"
+
+#include <boost/filesystem.hpp>
+#include <string>
+
+#include "mongo/base/string_data.h"
+#include "mongo/db/client.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/constants.h"
+#include "mongo/db/ftdc/file_reader.h"
+#include "mongo/db/jsobj.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/util/assert_util.h"
+#include "mongo/util/log.h"
+#include "mongo/util/mongoutils/str.h"
+#include "mongo/util/time_support.h"
+
+namespace mongo {
+
+// TODO: See time_support.cpp for details on this interposition
+#if defined(_MSC_VER) && _MSC_VER < 1900
+#define snprintf _snprintf
+#endif
+
+FTDCFileManager::FTDCFileManager(const FTDCConfig* config,
+ const boost::filesystem::path& path,
+ FTDCCollectorCollection* collection)
+ : _config(config), _writer(_config), _path(path), _rotateCollectors(collection) {}
+
+FTDCFileManager::~FTDCFileManager() {
+ close();
+}
+
+StatusWith<std::unique_ptr<FTDCFileManager>> FTDCFileManager::create(
+ const FTDCConfig* config,
+ const boost::filesystem::path& path,
+ FTDCCollectorCollection* collection,
+ Client* client) {
+ const boost::filesystem::path dir = boost::filesystem::absolute(path);
+
+ if (!boost::filesystem::exists(dir)) {
+ // Create the directory
+ if (!boost::filesystem::create_directories(dir)) {
+ return {ErrorCodes::NonExistentPath,
+ str::stream() << "\'" << dir.generic_string() << "\' could not be created."};
+ }
+ }
+
+ auto mgr =
+ std::unique_ptr<FTDCFileManager>(new FTDCFileManager(config, dir, std::move(collection)));
+
+ // Enumerate the metrics files
+ auto files = mgr->scanDirectory();
+
+ // Recover the interim file
+ auto interimDocs = mgr->recoverInterimFile();
+
+ // Open the archive file for writing
+ auto swFile = generateArchiveFileName(path, terseUTCCurrentTime());
+ if (!swFile.isOK()) {
+ return swFile.getStatus();
+ }
+
+ Status s = mgr->openArchiveFile(client, swFile.getValue(), interimDocs);
+ if (!s.isOK()) {
+ return s;
+ }
+
+ // Rotate as needed after we appended interim data to the archive file
+ mgr->trimDirectory(files);
+
+ return {std::move(mgr)};
+}
+
+std::vector<boost::filesystem::path> FTDCFileManager::scanDirectory() {
+ std::vector<boost::filesystem::path> files;
+
+ boost::filesystem::directory_iterator di(_path);
+ for (; di != boost::filesystem::directory_iterator(); di++) {
+ boost::filesystem::directory_entry& de = *di;
+ auto filename = de.path().filename();
+
+ std::string str = filename.generic_string();
+ if (str.compare(0, strlen(kFTDCArchiveFile) - 1, kFTDCArchiveFile) == 0 &&
+ str != kFTDCInterimFile) {
+ files.emplace_back(_path / filename);
+ }
+ }
+
+ std::sort(files.begin(), files.end());
+
+ return files;
+}
+
+StatusWith<boost::filesystem::path> FTDCFileManager::generateArchiveFileName(
+ const boost::filesystem::path& path, StringData suffix) {
+ auto fileName = path;
+ fileName /= std::string(kFTDCArchiveFile);
+ fileName += std::string(".");
+ fileName += suffix.toString();
+
+ if (boost::filesystem::exists(path)) {
+ // TODO: keep track of a high watermark for the current suffix so that after rotate the
+ // counter does not reset.
+ for (std::uint32_t i = 0; i < FTDCConfig::kMaxFileUniqifier; ++i) {
+ char buf[20];
+
+ // Use leading zeros so the numbers sort lexigraphically
+ int ret = snprintf(&buf[0], sizeof(buf), "%05u", i);
+ invariant(ret > 0 && ret < static_cast<int>((sizeof(buf) - 1)));
+
+ auto fileNameUnique = fileName;
+ fileNameUnique += std::string("-") + &buf[0];
+
+ if (!boost::filesystem::exists(fileNameUnique)) {
+ return fileNameUnique;
+ }
+ }
+
+ return {ErrorCodes::InvalidPath,
+ "Maximum limit reached for FTDC files in a second. The maximum file uniqifier has "
+ "been reached."};
+ }
+
+ return {fileName};
+}
+
+Status FTDCFileManager::openArchiveFile(
+ Client* client,
+ const boost::filesystem::path& path,
+ const std::vector<std::tuple<FTDCBSONUtil::FTDCType, BSONObj>>& docs) {
+ auto sOpen = _writer.open(path);
+ if (!sOpen.isOK()) {
+ return sOpen;
+ }
+
+ // Append any old interim records
+ for (auto& pair : docs) {
+ if (std::get<0>(pair) == FTDCBSONUtil::FTDCType::kMetadata) {
+ Status s = _writer.writeMetadata(std::get<1>(pair));
+
+ if (!s.isOK()) {
+ return s;
+ }
+ } else {
+ Status s = _writer.writeSample(std::get<1>(pair));
+
+ if (!s.isOK()) {
+ return s;
+ }
+ }
+ }
+
+ // After the system restarts or a new file has been started,
+ // collect one-time information
+ // This is appened after the file is opened to ensure a user can determine which bson objects
+ // where collected from which server instance.
+ BSONObj o = _rotateCollectors->collect(client);
+ if (!o.isEmpty()) {
+ Status s = _writer.writeMetadata(o);
+
+ if (!s.isOK()) {
+ return s;
+ }
+ }
+
+ return Status::OK();
+}
+
+void FTDCFileManager::trimDirectory(std::vector<boost::filesystem::path>& files) {
+ std::uint64_t maxSize = _config->maxDirectorySizeBytes;
+ std::uint64_t size = 0;
+
+ dassert(std::is_sorted(files.begin(), files.end()));
+
+ for (auto it = files.rbegin(); it != files.rend(); ++it) {
+ size += boost::filesystem::file_size(*it);
+
+ if (size >= maxSize) {
+ LOG(1) << "Cleaning file over full-time diagnostic data capture quota, file: "
+ << (*it).generic_string();
+ boost::filesystem::remove(*it);
+ }
+ }
+}
+
+std::vector<std::tuple<FTDCBSONUtil::FTDCType, BSONObj>> FTDCFileManager::recoverInterimFile() {
+ decltype(recoverInterimFile()) docs;
+
+ auto interimFile = FTDCUtil::getInterimFile(_path);
+
+ // Nothing to do if it does not exist
+ if (!boost::filesystem::exists(interimFile)) {
+ return docs;
+ }
+
+ size_t size = boost::filesystem::file_size(interimFile);
+ if (size == 0) {
+ return docs;
+ }
+
+ FTDCFileReader read;
+ auto s = read.open(interimFile);
+ if (!s.isOK()) {
+ log() << "Unclean full-time diagnostic data capture shutdown detected, found interim file, "
+ "but failed "
+ "to open it, some "
+ "metrics may have been lost. " << s;
+
+ // Note: We ignore any actual errors as reading from the interim files is a best-effort
+ return docs;
+ }
+
+ StatusWith<bool> m = read.hasNext();
+ for (; m.isOK() && m.getValue(); m = read.hasNext()) {
+ auto pair = read.next();
+ docs.emplace_back(std::tuple<FTDCBSONUtil::FTDCType, BSONObj>(
+ std::get<0>(pair), std::get<1>(pair).getOwned()));
+ }
+
+ // Warn if the interim file was corrupt or we had an unclean shutdown
+ if (!m.isOK() || !docs.empty()) {
+ log() << "Unclean full-time diagnostic data capture shutdown detected, found interim file, "
+ "some "
+ "metrics may have been lost. " << m.getStatus();
+ }
+
+ // Note: We ignore any actual errors as reading from the interim files is a best-effort
+ return docs;
+}
+
+Status FTDCFileManager::rotate(Client* client) {
+ auto s = _writer.close();
+ if (!s.isOK()) {
+ return s;
+ }
+
+ auto files = scanDirectory();
+
+ // Rotate as needed
+ trimDirectory(files);
+
+ auto swFile = generateArchiveFileName(_path, terseUTCCurrentTime());
+ if (!swFile.isOK()) {
+ return swFile.getStatus();
+ }
+
+ return openArchiveFile(client, swFile.getValue(), {});
+}
+
+Status FTDCFileManager::writeSampleAndRotateIfNeeded(Client* client, const BSONObj& sample) {
+ Status s = _writer.writeSample(sample);
+
+ if (!s.isOK()) {
+ return s;
+ }
+
+ if (_writer.getSize() > _config->maxFileSizeBytes) {
+ return rotate(client);
+ }
+
+ return Status::OK();
+}
+
+Status FTDCFileManager::close() {
+ return _writer.close();
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_manager.h b/src/mongo/db/ftdc/file_manager.h
new file mode 100644
index 00000000000..3342a1877cf
--- /dev/null
+++ b/src/mongo/db/ftdc/file_manager.h
@@ -0,0 +1,147 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <boost/filesystem/path.hpp>
+#include <memory>
+#include <tuple>
+#include <vector>
+
+#include "mongo/base/disallow_copying.h"
+#include "mongo/base/status.h"
+#include "mongo/base/string_data.h"
+#include "mongo/db/ftdc/collector.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/file_writer.h"
+#include "mongo/db/ftdc/util.h"
+#include "mongo/db/jsobj.h"
+
+namespace mongo {
+
+class Client;
+
+/**
+ * Manages a directory full of archive files, and an interim file.
+ *
+ * Manages file rotation, and directory size management.
+ */
+class FTDCFileManager {
+ MONGO_DISALLOW_COPYING(FTDCFileManager);
+
+public:
+ ~FTDCFileManager();
+
+ /**
+ * Creates the directory if it does not exist.
+ * NOTE: This must be run on a thread with a Client context, i.e., not a static initializer.
+ *
+ * Collectors are used to collect data to be stored as metadata on file rotation or system
+ * restart.
+ *
+ * Recovers data from the interim file as needed.
+ * Rotates files if needed.
+ */
+ static StatusWith<std::unique_ptr<FTDCFileManager>> create(const FTDCConfig* config,
+ const boost::filesystem::path& path,
+ FTDCCollectorCollection* collection,
+ Client* client);
+
+ /**
+ * Rotates files
+ */
+ Status rotate(Client* client);
+
+ /**
+ * Writes a sample to disk via FTDCFileWriter.
+ *
+ * Rotates files as needed.
+ */
+ Status writeSampleAndRotateIfNeeded(Client* client, const BSONObj& sample);
+
+ /**
+ * Closes the current file manager down.
+ */
+ Status close();
+
+public:
+ /**
+ * Generate a new file name for the archive.
+ * Public for use by unit tests only.
+ */
+ static StatusWith<boost::filesystem::path> generateArchiveFileName(
+ const boost::filesystem::path& path, StringData suffix);
+
+private:
+ FTDCFileManager(const FTDCConfig* config,
+ const boost::filesystem::path& path,
+ FTDCCollectorCollection* collection);
+
+ /**
+ * Gets a list of metrics files in a directory.
+ */
+ std::vector<boost::filesystem::path> scanDirectory();
+
+ /**
+ * Recover the interim file.
+ *
+ * Checks if the file is non-empty, and if so appends it the archive file.
+ */
+ std::vector<std::tuple<FTDCBSONUtil::FTDCType, BSONObj>> recoverInterimFile();
+
+ /**
+ * Removes the oldest files if the directory is over quota
+ */
+ void trimDirectory(std::vector<boost::filesystem::path>& files);
+
+ /**
+ * Open a new file for writing.
+ *
+ * Steps:
+ * 1. Writes any recovered interim file samples into the file.
+ * 2. Appends file rotation collectors upon opening the file.
+ */
+ Status openArchiveFile(Client* client,
+ const boost::filesystem::path& path,
+ const std::vector<std::tuple<FTDCBSONUtil::FTDCType, BSONObj>>& docs);
+
+private:
+ // config to use
+ const FTDCConfig* const _config;
+
+ // file to log samples to
+ FTDCFileWriter _writer;
+
+ // Path of metrics directory
+ boost::filesystem::path _path;
+
+ // collection of collectors to add to new files on rotation, and server restart
+ FTDCCollectorCollection* const _rotateCollectors;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_manager_test.cpp b/src/mongo/db/ftdc/file_manager_test.cpp
new file mode 100644
index 00000000000..bd4d81a1b87
--- /dev/null
+++ b/src/mongo/db/ftdc/file_manager_test.cpp
@@ -0,0 +1,320 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include <boost/filesystem.hpp>
+#include <algorithm>
+#include <iostream>
+#include <string>
+
+#include "mongo/base/init.h"
+#include "mongo/bson/bson_validate.h"
+#include "mongo/bson/bsonmisc.h"
+#include "mongo/bson/bsonobjbuilder.h"
+#include "mongo/db/client.h"
+#include "mongo/db/ftdc/collector.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/file_manager.h"
+#include "mongo/db/ftdc/file_writer.h"
+#include "mongo/db/ftdc/ftdc_test.h"
+#include "mongo/db/jsobj.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/unittest/temp_dir.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+
+// Test a full buffer
+TEST(FTDCFileManagerTest, TestFull) {
+ Client* client = &cc();
+ FTDCConfig c;
+ c.maxFileSizeBytes = 1000;
+ c.maxDirectorySizeBytes = 3000;
+ c.maxSamplesPerInterimMetricChunk = 1;
+
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path dir(tempdir.path());
+ createDirectoryClean(dir);
+
+ FTDCCollectorCollection rotate;
+ auto swMgr = FTDCFileManager::create(&c, dir, &rotate, client);
+ ASSERT_OK(swMgr.getStatus());
+ auto mgr = std::move(swMgr.getValue());
+
+ // Test a large numbers of zeros, and incremental numbers in a full buffer
+ for (int j = 0; j < 4; j++) {
+ ASSERT_OK(
+ mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 3230792343LL << "key2" << 235135)));
+
+ for (size_t i = 0; i <= FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault - 2; i++) {
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(
+ client,
+ BSON("name"
+ << "joe"
+ << "key1" << static_cast<long long int>(i * j * 37) << "key2"
+ << static_cast<long long int>(i * (645 << j)))));
+ }
+
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+
+ // Add Value
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+ }
+
+ mgr->close();
+
+ auto files = scanDirectory(dir);
+
+ int sum = 0;
+ for (auto& file : files) {
+ int fs = boost::filesystem::file_size(file);
+ ASSERT_TRUE(fs < c.maxFileSizeBytes * 1.10);
+ if (file.generic_string().find("interim") == std::string::npos) {
+ sum += fs;
+ }
+ }
+
+ ASSERT_TRUE(sum < c.maxDirectorySizeBytes * 1.10);
+}
+
+void ValidateInterimFileHasData(const boost::filesystem::path& dir, bool hasData) {
+ char buf[sizeof(std::int32_t)];
+
+ auto interimFile = FTDCUtil::getInterimFile(dir);
+
+ std::fstream stream(interimFile.c_str());
+ stream.read(&buf[0], sizeof(buf));
+
+ ASSERT_EQUALS(4, stream.gcount());
+ std::uint32_t bsonLength = ConstDataView(buf).read<LittleEndian<std::int32_t>>();
+
+ ASSERT_EQUALS(static_cast<bool>(bsonLength), hasData);
+}
+
+// Test a normal restart
+TEST(FTDCFileManagerTest, TestNormalRestart) {
+ Client* client = &cc();
+ FTDCConfig c;
+ c.maxFileSizeBytes = 1000;
+ c.maxDirectorySizeBytes = 3000;
+
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path dir(tempdir.path());
+
+ createDirectoryClean(dir);
+
+ for (int i = 0; i < 3; i++) {
+ // Do a few cases of stop and start to ensure it works as expected
+ FTDCCollectorCollection rotate;
+ auto swMgr = FTDCFileManager::create(&c, dir, &rotate, client);
+ ASSERT_OK(swMgr.getStatus());
+ auto mgr = std::move(swMgr.getValue());
+
+ // Test a large numbers of zeros, and incremental numbers in a full buffer
+ for (int j = 0; j < 4; j++) {
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 3230792343LL << "key2"
+ << 235135)));
+
+ for (size_t i = 0; i <= FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault - 2; i++) {
+ ASSERT_OK(
+ mgr->writeSampleAndRotateIfNeeded(
+ client,
+ BSON("name"
+ << "joe"
+ << "key1" << static_cast<long long int>(i * j * 37) << "key2"
+ << static_cast<long long int>(i * (645 << j)))));
+ }
+
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+
+ // Add Value
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+ }
+
+ mgr->close();
+
+ // Validate the interim file does not have data
+ ValidateInterimFileHasData(dir, false);
+ }
+}
+
+// Test a restart after a crash with a corrupt archive file
+TEST(FTDCFileManagerTest, TestCorruptCrashRestart) {
+ Client* client = &cc();
+ FTDCConfig c;
+ c.maxFileSizeBytes = 1000;
+ c.maxDirectorySizeBytes = 3000;
+
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path dir(tempdir.path());
+
+ createDirectoryClean(dir);
+
+ for (int i = 0; i < 2; i++) {
+ // Do a few cases of stop and start to ensure it works as expected
+ FTDCCollectorCollection rotate;
+ auto swMgr = FTDCFileManager::create(&c, dir, &rotate, client);
+ ASSERT_OK(swMgr.getStatus());
+ auto mgr = std::move(swMgr.getValue());
+
+ // Test a large numbers of zeros, and incremental numbers in a full buffer
+ for (int j = 0; j < 4; j++) {
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 3230792343LL << "key2"
+ << 235135)));
+
+ for (size_t i = 0; i <= FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault - 2; i++) {
+ ASSERT_OK(
+ mgr->writeSampleAndRotateIfNeeded(
+ client,
+ BSON("name"
+ << "joe"
+ << "key1" << static_cast<long long int>(i * j * 37) << "key2"
+ << static_cast<long long int>(i * (645 << j)))));
+ }
+
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+
+ // Add Value
+ ASSERT_OK(mgr->writeSampleAndRotateIfNeeded(client,
+ BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45)));
+ }
+
+ mgr->close();
+
+ auto swFile = FTDCFileManager::generateArchiveFileName(dir, "0test-crash");
+ ASSERT_OK(swFile);
+
+ std::ofstream stream(swFile.getValue().c_str());
+ // This test case caused us to allocate more memory then the size of the file the first time
+ // I tried it
+ stream << "Hello World";
+
+ stream.close();
+ }
+}
+
+// Test a restart with a good interim file, and validate we have all the data
+TEST(FTDCFileManagerTest, TestNormalCrashInterim) {
+ Client* client = &cc();
+ FTDCConfig c;
+ c.maxSamplesPerInterimMetricChunk = 3;
+ c.maxFileSizeBytes = 10 * 1024 * 1024;
+ c.maxDirectorySizeBytes = 10 * 1024 * 1024;
+
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path dir(tempdir.path());
+
+ createDirectoryClean(dir);
+
+ BSONObj mdoc1 = BSON("name"
+ << "some_metadata"
+ << "key1" << 34 << "something" << 98);
+
+ BSONObj sdoc1 = BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45);
+ BSONObj sdoc2 = BSON("name"
+ << "joe"
+ << "key3" << 34 << "key5" << 45);
+
+ auto swFile = FTDCFileManager::generateArchiveFileName(dir, "0test-crash");
+ ASSERT_OK(swFile);
+
+ {
+ FTDCFileWriter writer(&c);
+
+ ASSERT_OK(writer.open(swFile.getValue()));
+
+ ASSERT_OK(writer.writeMetadata(mdoc1));
+
+ ASSERT_OK(writer.writeSample(sdoc1));
+ ASSERT_OK(writer.writeSample(sdoc1));
+ ASSERT_OK(writer.writeSample(sdoc2));
+ ASSERT_OK(writer.writeSample(sdoc2));
+ ASSERT_OK(writer.writeSample(sdoc2));
+ ASSERT_OK(writer.writeSample(sdoc2));
+
+ // leave some data in the interim file
+ writer.closeWithoutFlushForTest();
+ }
+
+ // Validate the interim file has data
+ ValidateInterimFileHasData(dir, true);
+
+ // Let the manager run the recovery over the interim file
+ {
+ FTDCCollectorCollection rotate;
+ auto swMgr = FTDCFileManager::create(&c, dir, &rotate, client);
+ ASSERT_OK(swMgr.getStatus());
+ auto mgr = std::move(swMgr.getValue());
+ ASSERT_OK(mgr->close());
+ }
+
+ // Validate the file manager rolled over the changes to the current archive file
+ // and did not start a new archive file.
+ auto files = scanDirectory(dir);
+
+ std::sort(files.begin(), files.end());
+
+ // Validate old file
+ std::vector<BSONObj> docs1 = {mdoc1, sdoc1, sdoc1};
+ ValidateDocumentList(files[0], docs1);
+
+ // Validate new file
+ std::vector<BSONObj> docs2 = {sdoc2, sdoc2, sdoc2, sdoc2};
+ ValidateDocumentList(files[1], docs2);
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_reader.cpp b/src/mongo/db/ftdc/file_reader.cpp
new file mode 100644
index 00000000000..ca850c374fa
--- /dev/null
+++ b/src/mongo/db/ftdc/file_reader.cpp
@@ -0,0 +1,216 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/ftdc/file_reader.h"
+
+#include <boost/filesystem.hpp>
+#include <fstream>
+
+#include "mongo/base/data_range_cursor.h"
+#include "mongo/base/data_type_validated.h"
+#include "mongo/bson/bsonmisc.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/util.h"
+#include "mongo/db/jsobj.h"
+#include "mongo/util/mongoutils/str.h"
+
+namespace mongo {
+
+FTDCFileReader::~FTDCFileReader() {
+ _stream.close();
+}
+
+StatusWith<bool> FTDCFileReader::hasNext() {
+ while (true) {
+ if (_state == State::kNeedsDoc) {
+ if (_stream.eof()) {
+ return {false};
+ }
+
+ auto swDoc = readDocument();
+ if (!swDoc.isOK()) {
+ return swDoc.getStatus();
+ }
+
+ if (swDoc.getValue().isEmpty()) {
+ return {false};
+ }
+
+ _parent = swDoc.getValue();
+
+ // metadata or metrics?
+ auto swType = FTDCBSONUtil::getBSONDocumentType(_parent);
+ if (!swType.isOK()) {
+ return swType.getStatus();
+ }
+
+ FTDCBSONUtil::FTDCType type = swType.getValue();
+
+ if (type == FTDCBSONUtil::FTDCType::kMetadata) {
+ _state = State::kMetadataDoc;
+
+ auto swMetadata = FTDCBSONUtil::getBSONDocumentFromMetadataDoc(_parent);
+ if (!swMetadata.isOK()) {
+ return swMetadata.getStatus();
+ }
+
+ _metadata = swMetadata.getValue();
+ } else if (type == FTDCBSONUtil::FTDCType::kMetricChunk) {
+ _state = State::kMetricChunk;
+
+ auto swDocs = FTDCBSONUtil::getMetricsFromMetricDoc(_parent, &_decompressor);
+ if (!swDocs.isOK()) {
+ return swDocs.getStatus();
+ }
+
+ _docs = swDocs.getValue();
+
+ // There is always at least the reference document
+ _pos = 0;
+ }
+
+ return {true};
+ }
+
+ // We previously returned a metadata document, now we need another document from disk
+ if (_state == State::kMetadataDoc) {
+ _state = State::kNeedsDoc;
+ continue;
+ }
+
+ // If we have a metric chunk, return the next document in the chunk until the chunk is
+ // exhausted
+ if (_state == State::kMetricChunk) {
+ if (_pos + 1 == _docs.size()) {
+ _state = State::kNeedsDoc;
+ continue;
+ }
+
+ _pos++;
+
+ return {true};
+ }
+ }
+}
+
+std::tuple<FTDCBSONUtil::FTDCType, const BSONObj&> FTDCFileReader::next() {
+ dassert(_state == State::kMetricChunk || _state == State::kMetadataDoc);
+
+ if (_state == State::kMetadataDoc) {
+ return std::tuple<FTDCBSONUtil::FTDCType, const BSONObj&>(FTDCBSONUtil::FTDCType::kMetadata,
+ _metadata);
+ }
+
+ if (_state == State::kMetricChunk) {
+ return std::tuple<FTDCBSONUtil::FTDCType, const BSONObj&>(
+ FTDCBSONUtil::FTDCType::kMetricChunk, _docs[_pos]);
+ }
+
+ MONGO_UNREACHABLE;
+}
+
+StatusWith<BSONObj> FTDCFileReader::readDocument() {
+ if (!_stream.is_open()) {
+ return {ErrorCodes::FileNotOpen, "open() needs to be called first."};
+ }
+
+ char buf[sizeof(std::int32_t)];
+
+ _stream.read(buf, sizeof(buf));
+
+ if (sizeof(buf) != _stream.gcount()) {
+ // Did we read exactly zero bytes and hit the eof?
+ // Then return an empty document to indicate we are done reading the file.
+ if (0 == _stream.gcount() && _stream.eof()) {
+ return BSONObj();
+ }
+
+ return {ErrorCodes::FileStreamFailed,
+ str::stream() << "Failed to read 4 bytes from file \'" << _file.generic_string()
+ << "\'"};
+ }
+
+ std::uint32_t bsonLength = ConstDataView(buf).read<LittleEndian<std::int32_t>>();
+
+ // Reads past the end of the file will be caught below
+ // The interim file sentinel is 8 bytes of zero.
+ if (bsonLength == 0) {
+ return BSONObj();
+ }
+
+ // Reads past the end of the file will be caught below
+ if (bsonLength > _fileSize || bsonLength < BSONObj::kMinBSONLength) {
+ return {ErrorCodes::InvalidLength,
+ str::stream() << "Invalid BSON length found in file \'" << _file.generic_string()
+ << "\'"};
+ }
+
+ // Read the BSON document
+ _buffer.resize(bsonLength);
+
+ // Stuff the length into the front
+ memcpy(_buffer.data(), buf, sizeof(std::int32_t));
+
+ // Read the length - 4 bytes from the file
+ std::int32_t readSize = bsonLength - sizeof(std::int32_t);
+
+ _stream.read(_buffer.data() + sizeof(std::int32_t), readSize);
+
+ if (readSize != _stream.gcount()) {
+ return {ErrorCodes::FileStreamFailed,
+ str::stream() << "Failed to read " << readSize << " bytes from file \'"
+ << _file.generic_string() << "\'"};
+ }
+
+ ConstDataRange cdr(_buffer.data(), _buffer.data() + bsonLength);
+
+ // TODO: Validated only validates objects based on a flag which is the default at the moment
+ auto swl = cdr.read<Validated<BSONObj>>();
+ if (!swl.isOK()) {
+ return swl.getStatus();
+ }
+
+ return {swl.getValue().val};
+}
+
+Status FTDCFileReader::open(const boost::filesystem::path& file) {
+ _stream.open(file.c_str(), std::ios_base::in | std::ios_base::binary);
+ if (!_stream.is_open()) {
+ return Status(ErrorCodes::FileStreamFailed, "Failed to open file " + file.generic_string());
+ }
+
+ _fileSize = boost::filesystem::file_size(file);
+
+ _file = file;
+
+ return Status::OK();
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_reader.h b/src/mongo/db/ftdc/file_reader.h
new file mode 100644
index 00000000000..c1a99d10bf2
--- /dev/null
+++ b/src/mongo/db/ftdc/file_reader.h
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <boost/filesystem/path.hpp>
+#include <boost/optional.hpp>
+#include <fstream>
+#include <stddef.h>
+#include <vector>
+
+#include "mongo/base/disallow_copying.h"
+#include "mongo/base/status.h"
+#include "mongo/base/status_with.h"
+#include "mongo/db/ftdc/decompressor.h"
+#include "mongo/db/ftdc/util.h"
+#include "mongo/db/jsobj.h"
+
+namespace mongo {
+
+/**
+ * Reads a file, either an archive stream or interim file
+ *
+ * Does not recover interim files into archive files.
+ */
+class FTDCFileReader {
+ MONGO_DISALLOW_COPYING(FTDCFileReader);
+
+public:
+ FTDCFileReader() : _state(State::kNeedsDoc) {}
+ ~FTDCFileReader();
+
+ /**
+ * Open the specified file
+ */
+ Status open(const boost::filesystem::path& file);
+
+ /**
+ * Returns true if their are more records in the file.
+ * Returns false if the end of the file has been reached.
+ * Return other error codes if the file is corrupt.
+ */
+ StatusWith<bool> hasNext();
+
+ /**
+ * Returns the next document.
+ * Metadata documents are unowned.
+ * Metric documents are owned.
+ */
+ std::tuple<FTDCBSONUtil::FTDCType, const BSONObj&> next();
+
+private:
+ /**
+ * Read a document from the file. If the file is corrupt, returns an appropriate status.
+ */
+ StatusWith<BSONObj> readDocument();
+
+private:
+ FTDCDecompressor _decompressor;
+
+ /**
+ * Internal state of file reading state machine.
+ *
+ * +--------------+ +--------------+
+ * +> | kNeedsDoc | <--> | kMetadataDoc |
+ * | +--------------+ +--------------+
+ * |
+ * | +----------+
+ * | v |
+ * | +--------------+
+ * +> | kMetricChunk |
+ * +--------------+
+ */
+ enum class State {
+
+ /**
+ * Indicates that we need to read another document from disk
+ */
+ kNeedsDoc,
+
+ /**
+ * Indicates that are processing a metric chunk, and have one or more documents to return.
+ */
+ kMetricChunk,
+
+ /**
+ * Indicates that we need to read another document from disk
+ */
+ kMetadataDoc,
+ };
+
+ State _state{State::kNeedsDoc};
+
+ // Current position in the _docs array.
+ std::size_t _pos{0};
+
+ // Current set of metrics documents
+ std::vector<BSONObj> _docs;
+
+ // Current metadata document - unowned
+ BSONObj _metadata;
+
+ // Parent document
+ BSONObj _parent;
+
+ // Buffer of data read from disk
+ std::vector<char> _buffer;
+
+ // File name
+ boost::filesystem::path _file;
+
+ // Size of file on disk
+ std::size_t _fileSize{0};
+
+ // Input file stream
+ std::ifstream _stream;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_writer.cpp b/src/mongo/db/ftdc/file_writer.cpp
new file mode 100644
index 00000000000..4143c88bd9a
--- /dev/null
+++ b/src/mongo/db/ftdc/file_writer.cpp
@@ -0,0 +1,210 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kFTDC
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/ftdc/file_writer.h"
+
+#include <boost/filesystem.hpp>
+#include <fstream>
+#include <string>
+
+#include "mongo/base/string_data.h"
+#include "mongo/db/ftdc/compressor.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/util.h"
+#include "mongo/db/jsobj.h"
+#include "mongo/util/assert_util.h"
+#include "mongo/util/log.h"
+
+namespace mongo {
+
+FTDCFileWriter::~FTDCFileWriter() {
+ close();
+}
+
+Status FTDCFileWriter::open(const boost::filesystem::path& file) {
+ if (_archiveStream.is_open()) {
+ return {ErrorCodes::FileAlreadyOpen, "FTDCFileWriter is already open."};
+ }
+
+ _archiveFile = file;
+
+ // Disable file buffering
+ _archiveStream.rdbuf()->pubsetbuf(0, 0);
+
+ // Ideally, we create a file from scratch via O_CREAT but there is not portable way via C++
+ // iostreams to do this.
+ _archiveStream.open(_archiveFile.c_str(),
+ std::ios_base::out | std::ios_base::binary | std::ios_base::app);
+
+ if (!_archiveStream.is_open()) {
+ return Status(ErrorCodes::FileNotOpen,
+ "Failed to open archive file " + file.generic_string());
+ }
+
+ // Set internal size tracking to reflect the current file size
+ _size = boost::filesystem::file_size(file);
+ _sizeInterim = 0;
+
+ _interimFile = FTDCUtil::getInterimFile(file);
+
+ // Disable file buffering
+ _interimStream.rdbuf()->pubsetbuf(0, 0);
+
+ _interimStream.open(_interimFile.c_str(),
+ std::ios_base::out | std::ios_base::binary | std::ios_base::trunc);
+
+ if (!_interimStream.is_open()) {
+ return Status(ErrorCodes::FileNotOpen,
+ "Failed to open interim file " + _interimFile.generic_string());
+ }
+
+ _compressor.reset();
+
+ return Status::OK();
+}
+
+Status FTDCFileWriter::writeInterimFileBuffer(ConstDataRange buf) {
+ _interimStream.seekp(0);
+
+ _interimStream.write(buf.data(), buf.length());
+
+ if (_interimStream.fail()) {
+ return {
+ ErrorCodes::FileStreamFailed,
+ str::stream()
+ << "Failed to write to interim file buffer for full-time diagnostic data capture: "
+ << _interimFile.generic_string()};
+ }
+
+ _sizeInterim = buf.length();
+
+ return Status::OK();
+}
+
+Status FTDCFileWriter::writeArchiveFileBuffer(ConstDataRange buf) {
+ _archiveStream.write(buf.data(), buf.length());
+
+ if (_archiveStream.fail()) {
+ return {
+ ErrorCodes::FileStreamFailed,
+ str::stream()
+ << "Failed to write to archive file buffer for full-time diagnostic data capture: "
+ << _archiveFile.generic_string()};
+ }
+
+ _size += buf.length();
+
+ return Status::OK();
+}
+
+Status FTDCFileWriter::writeMetadata(const BSONObj& metadata) {
+ BSONObj wrapped = FTDCBSONUtil::createBSONMetadataDocument(metadata);
+
+ return writeArchiveFileBuffer({wrapped.objdata(), static_cast<size_t>(wrapped.objsize())});
+}
+
+Status FTDCFileWriter::writeSample(const BSONObj& sample) {
+ auto ret = _compressor.addSample(sample);
+
+ if (!ret.isOK()) {
+ return ret.getStatus();
+ }
+
+ if (ret.getValue().is_initialized()) {
+ return flush(std::get<0>(ret.getValue().get()));
+ }
+
+ if (_compressor.getSampleCount() != 0 &&
+ (_compressor.getSampleCount() % _config->maxSamplesPerInterimMetricChunk) == 0) {
+ // Check if we want to do a partial write to the intrim buffer
+ auto swBuf = _compressor.getCompressedSamples();
+ if (!swBuf.isOK()) {
+ return swBuf.getStatus();
+ }
+
+ BSONObj o = FTDCBSONUtil::createBSONMetricChunkDocument(swBuf.getValue());
+ return writeInterimFileBuffer({o.objdata(), static_cast<size_t>(o.objsize())});
+ }
+
+ return Status::OK();
+}
+
+Status FTDCFileWriter::flush(const boost::optional<ConstDataRange>& range) {
+ if (!range.is_initialized()) {
+ if (_compressor.hasDataToFlush()) {
+ StatusWith<ConstDataRange> swBuf = _compressor.getCompressedSamples();
+
+ if (!swBuf.isOK()) {
+ return swBuf.getStatus();
+ }
+
+ BSONObj o = FTDCBSONUtil::createBSONMetricChunkDocument(swBuf.getValue());
+ Status s = writeArchiveFileBuffer({o.objdata(), static_cast<size_t>(o.objsize())});
+
+ if (!s.isOK()) {
+ return s;
+ }
+ }
+ } else {
+ BSONObj o = FTDCBSONUtil::createBSONMetricChunkDocument(range.get());
+ Status s = writeArchiveFileBuffer({o.objdata(), static_cast<size_t>(o.objsize())});
+
+ if (!s.isOK()) {
+ return s;
+ }
+ }
+
+ // Nuke the interim stash by writes 8 bytes of zero to the head of the file
+ // We want to avoid truncating the file since we want to minimize I/O
+ char zero[8] = {};
+ return writeInterimFileBuffer({&zero[0], sizeof(zero)});
+}
+
+Status FTDCFileWriter::close() {
+ if (_interimStream.is_open()) {
+ Status s = flush(boost::none_t());
+
+ _archiveStream.close();
+ _interimStream.close();
+
+ return s;
+ }
+
+ return Status::OK();
+}
+
+void FTDCFileWriter::closeWithoutFlushForTest() {
+ _archiveStream.close();
+ _interimStream.close();
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_writer.h b/src/mongo/db/ftdc/file_writer.h
new file mode 100644
index 00000000000..00327c9f3ed
--- /dev/null
+++ b/src/mongo/db/ftdc/file_writer.h
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <boost/filesystem/path.hpp>
+#include <cstddef>
+#include <cstdint>
+#include <fstream>
+#include <vector>
+
+#include "mongo/base/status.h"
+#include "mongo/db/ftdc/compressor.h"
+#include "mongo/db/jsobj.h"
+
+namespace mongo {
+
+/**
+ * Manages writing to an append only archive file, and an interim file.
+ * - The archive file is designed to write complete metric chunks.
+ * - The interim file writes smaller chunks in case of process failure.
+ *
+ * Note: This class never reads from the interim file. It is the callers responsibility to check
+ * for unclean shutdown as this file. An unclean shutdown will mean there is valid data in the
+ * interim file. If the shutdown is clean, the interim file will contain zeros as the leading
+ * 8 bytes instead of a valid BSON length.
+ *
+ * The chunks in the archive stream will have better compression since it compresses larger chunks
+ * of data.
+ *
+ * File format is compatible with mongodump as it is just a sequential series of bson documents
+ *
+ * File rotation and cleanup is not handled by this class.
+ */
+class FTDCFileWriter {
+ MONGO_DISALLOW_COPYING(FTDCFileWriter);
+
+public:
+ FTDCFileWriter(const FTDCConfig* config) : _config(config), _compressor(_config) {}
+ ~FTDCFileWriter();
+
+ /**
+ * Open both an archive file, and interim file.
+ */
+ Status open(const boost::filesystem::path& file);
+
+ /**
+ * Write a BSON document as a metadata type to the archive log.
+ */
+ Status writeMetadata(const BSONObj& metadata);
+
+ /**
+ * Write a sample to interim and/or archive log as needed.
+ */
+ Status writeSample(const BSONObj& sample);
+
+ /**
+ * Close all the files and shutdown cleanly by zeroing the beginning of the interim file.
+ */
+ Status close();
+
+ /**
+ * Get the size of data written to file. Size of file after file is closed due to effects of
+ * compression may be different.
+ */
+ std::size_t getSize() const {
+ return _size + _sizeInterim;
+ }
+
+public:
+ /**
+ * Test hook that closes the files without moving interim results to the archive log.
+ * Note: OS Buffers are still flushes correctly though.
+ */
+ void closeWithoutFlushForTest();
+
+private:
+ /**
+ * Flush all changes to disk.
+ */
+ Status flush(const boost::optional<ConstDataRange>&);
+
+ /**
+ * Write a buffer to the beginning of the interim file.
+ */
+ Status writeInterimFileBuffer(ConstDataRange buf);
+
+ /**
+ * Append a buffer to the archive file.
+ */
+ Status writeArchiveFileBuffer(ConstDataRange buf);
+
+private:
+ // Config
+ const FTDCConfig* const _config;
+
+ // Archive file name
+ boost::filesystem::path _archiveFile;
+
+ // Interim file name
+ boost::filesystem::path _interimFile;
+
+ // Append only archive stream
+ std::ofstream _archiveStream;
+
+ // Fixed size interim stream
+ std::ofstream _interimStream;
+
+ // FTDC compressor
+ FTDCCompressor _compressor;
+
+ // Size of archive file
+ std::size_t _size{0};
+
+ // Size of interim file
+ std::size_t _sizeInterim{0};
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/ftdc/file_writer_test.cpp b/src/mongo/db/ftdc/file_writer_test.cpp
new file mode 100644
index 00000000000..0a1a262fc50
--- /dev/null
+++ b/src/mongo/db/ftdc/file_writer_test.cpp
@@ -0,0 +1,265 @@
+/**
+ * Copyright (C) 2015 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include <boost/filesystem.hpp>
+
+#include "mongo/base/init.h"
+#include "mongo/bson/bsonmisc.h"
+#include "mongo/bson/bsonobjbuilder.h"
+#include "mongo/db/ftdc/config.h"
+#include "mongo/db/ftdc/file_reader.h"
+#include "mongo/db/ftdc/file_writer.h"
+#include "mongo/db/ftdc/ftdc_test.h"
+#include "mongo/db/jsobj.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/unittest/temp_dir.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+
+const char* kTestFile = "metrics.test";
+
+// File Sanity check
+TEST(FTDCFileTest, TestFileBasicMetadata) {
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path p(tempdir.path());
+ p /= kTestFile;
+
+ deleteFileIfNeeded(p);
+
+ BSONObj doc1 = BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45);
+ BSONObj doc2 = BSON("name"
+ << "joe"
+ << "key3" << 34 << "key5" << 45);
+
+ FTDCConfig config;
+ FTDCFileWriter writer(&config);
+
+ ASSERT_OK(writer.open(p));
+
+ ASSERT_OK(writer.writeMetadata(doc1));
+ ASSERT_OK(writer.writeMetadata(doc2));
+
+ writer.close();
+
+ FTDCFileReader reader;
+ ASSERT_OK(reader.open(p));
+
+ ASSERT_OK(reader.hasNext());
+
+ BSONObj doc1a = std::get<1>(reader.next());
+
+ ASSERT_TRUE(doc1 == doc1a);
+
+ ASSERT_OK(reader.hasNext());
+
+ BSONObj doc2a = std::get<1>(reader.next());
+
+ ASSERT_TRUE(doc2 == doc2a);
+
+ auto sw = reader.hasNext();
+ ASSERT_OK(sw);
+ ASSERT_EQUALS(sw.getValue(), false);
+}
+
+// File Sanity check
+TEST(FTDCFileTest, TestFileBasicCompress) {
+ unittest::TempDir tempdir("metrics_testpath");
+ boost::filesystem::path p(tempdir.path());
+ p /= kTestFile;
+
+ deleteFileIfNeeded(p);
+
+ BSONObj doc1 = BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45);
+ BSONObj doc2 = BSON("name"
+ << "joe"
+ << "key3" << 34 << "key5" << 45);
+
+ FTDCConfig config;
+ FTDCFileWriter writer(&config);
+
+ ASSERT_OK(writer.open(p));
+
+ ASSERT_OK(writer.writeSample(doc1));
+ ASSERT_OK(writer.writeSample(doc2));
+
+ writer.close();
+
+ FTDCFileReader reader;
+ ASSERT_OK(reader.open(p));
+
+ ASSERT_OK(reader.hasNext());
+
+ BSONObj doc1a = std::get<1>(reader.next());
+
+ ASSERT_TRUE(doc1 == doc1a);
+
+ ASSERT_OK(reader.hasNext());
+
+ BSONObj doc2a = std::get<1>(reader.next());
+
+ ASSERT_TRUE(doc2 == doc2a);
+
+ auto sw = reader.hasNext();
+ ASSERT_OK(sw);
+ ASSERT_EQUALS(sw.getValue(), false);
+}
+
+/**
+ * Validates all the data that gets written to file is returned as is
+ */
+class FileTestTie {
+public:
+ FileTestTie()
+ : _tempdir("metrics_testpath"),
+ _path(boost::filesystem::path(_tempdir.path()) / kTestFile),
+ _writer(&_config) {
+ deleteFileIfNeeded(_path);
+
+ ASSERT_OK(_writer.open(_path));
+ }
+
+ ~FileTestTie() {
+ validate();
+ }
+
+ void addSample(const BSONObj& sample) {
+ ASSERT_OK(_writer.writeSample(sample));
+ _docs.emplace_back(sample);
+ }
+
+private:
+ void validate(bool forceCompress = true) {
+ _writer.close();
+
+ ValidateDocumentList(_path, _docs);
+ }
+
+private:
+ unittest::TempDir _tempdir;
+ boost::filesystem::path _path;
+ FTDCConfig _config;
+ FTDCFileWriter _writer;
+ std::vector<BSONObj> _docs;
+};
+
+// Test various schema changes
+TEST(FTDCFileTest, TestSchemaChanges) {
+ FileTestTie c;
+
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 33 << "key2" << 42));
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45));
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45));
+
+ // Add Value
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45 << "key3" << 47));
+
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45 << "key3" << 47));
+
+ // Rename field
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key5" << 45 << "key3" << 47));
+
+ // Change type
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key5"
+ << "45"
+ << "key3" << 47));
+
+ // RemoveField
+ c.addSample(BSON("name"
+ << "joe"
+ << "key5"
+ << "45"
+ << "key3" << 47));
+}
+
+// Test a full buffer
+TEST(FTDCFileTest, TestFull) {
+ // Test a large numbers of zeros, and incremental numbers in a full buffer
+ for (int j = 0; j < 2; j++) {
+ FileTestTie c;
+
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 33 << "key2" << 42));
+
+ for (size_t i = 0; i <= FTDCConfig::kMaxSamplesPerArchiveMetricChunkDefault - 2; i++) {
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << static_cast<long long int>(i * j) << "key2" << 45));
+ }
+
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45));
+
+ // Add Value
+ c.addSample(BSON("name"
+ << "joe"
+ << "key1" << 34 << "key2" << 45));
+ }
+}
+
+// Test a bad file
+TEST(FTDCFileTest, TestBadFile) {
+ boost::filesystem::path p(kTestFile);
+
+ std::ofstream stream(p.c_str());
+ // This test case caused us to allocate more memory then the size of the file the first time I
+ // tried it
+ stream << "Hello World";
+
+ stream.close();
+
+ FTDCFileReader reader;
+ ASSERT_OK(reader.open(p));
+
+ auto sw = reader.hasNext();
+ ASSERT_NOT_OK(sw);
+}
+
+} // namespace mongo