diff options
author | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2015-08-17 11:53:38 -0400 |
---|---|---|
committer | Mark Benvenuto <mark.benvenuto@mongodb.com> | 2015-09-08 21:01:41 -0400 |
commit | 9271c08121a0f511b3326a8a2eca449a6a523309 (patch) | |
tree | 03f7362598c395dd64eb4c8a6b0ac59bf19f51d4 /src | |
parent | d608d9b34814a82a6d512a5817160a099bfed90f (diff) | |
download | mongo-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/SConscript | 5 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_manager.cpp | 301 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_manager.h | 147 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_manager_test.cpp | 320 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_reader.cpp | 216 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_reader.h | 144 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_writer.cpp | 210 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_writer.h | 144 | ||||
-rw-r--r-- | src/mongo/db/ftdc/file_writer_test.cpp | 265 |
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 |