Merge pull request #1071 from mapbox/1071-sqlite-avoid-crash
Gracefully handle SQLite errors
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <mbgl/platform/event.hpp>
|
||||
|
||||
#include <mbgl/util/std.hpp>
|
||||
#include <mbgl/util/noncopyable.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -12,7 +13,7 @@ namespace mbgl {
|
||||
|
||||
class Log {
|
||||
public:
|
||||
class Observer {
|
||||
class Observer : private util::noncopyable {
|
||||
public:
|
||||
virtual ~Observer() = default;
|
||||
|
||||
@@ -22,6 +23,7 @@ public:
|
||||
};
|
||||
|
||||
static void setObserver(std::unique_ptr<Observer> Observer);
|
||||
static std::unique_ptr<Observer> removeObserver();
|
||||
|
||||
private:
|
||||
template <typename T, size_t N>
|
||||
|
||||
@@ -38,6 +38,7 @@ private:
|
||||
void process(StopAction &action);
|
||||
|
||||
void createDatabase();
|
||||
void createSchema();
|
||||
|
||||
const std::string path;
|
||||
uv_loop_t *loop = nullptr;
|
||||
@@ -45,6 +46,7 @@ private:
|
||||
std::thread thread;
|
||||
std::unique_ptr<::mapbox::sqlite::Database> db;
|
||||
std::unique_ptr<::mapbox::sqlite::Statement> getStmt, putStmt, refreshStmt;
|
||||
bool schema = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -76,11 +76,16 @@ Statement::Statement(sqlite3 *db, const char *sql) {
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK_SQLITE_OK(err) \
|
||||
#define CHECK_SQLITE_OK_STMT(err, stmt) \
|
||||
if (err != SQLITE_OK) { \
|
||||
throw Exception { err, sqlite3_errmsg(sqlite3_db_handle(stmt)) }; \
|
||||
}
|
||||
|
||||
#define CHECK_SQLITE_OK(err) \
|
||||
if (err != SQLITE_OK) { \
|
||||
throw Exception { err, sqlite3_errstr(err) }; \
|
||||
}
|
||||
|
||||
Statement::Statement(Statement &&other) {
|
||||
*this = std::move(other);
|
||||
}
|
||||
@@ -92,8 +97,7 @@ Statement &Statement::operator=(Statement &&other) {
|
||||
|
||||
Statement::~Statement() {
|
||||
if (stmt) {
|
||||
const int err = sqlite3_finalize(stmt);
|
||||
CHECK_SQLITE_OK(err)
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,38 +105,38 @@ Statement::operator bool() const {
|
||||
return stmt != nullptr;
|
||||
}
|
||||
|
||||
#define BIND_3(type, value) \
|
||||
#define BIND_3(type, value, stmt) \
|
||||
assert(stmt); \
|
||||
const int err = sqlite3_bind_##type(stmt, offset, value); \
|
||||
CHECK_SQLITE_OK(err)
|
||||
CHECK_SQLITE_OK_STMT(err, stmt)
|
||||
|
||||
#define BIND_5(type, value, length, param) \
|
||||
#define BIND_5(type, value, length, param, stmt) \
|
||||
assert(stmt); \
|
||||
const int err = sqlite3_bind_##type(stmt, offset, value, length, param); \
|
||||
CHECK_SQLITE_OK(err)
|
||||
CHECK_SQLITE_OK_STMT(err, stmt)
|
||||
|
||||
template <> void Statement::bind(int offset, int value) {
|
||||
BIND_3(int, value)
|
||||
BIND_3(int, value, stmt)
|
||||
}
|
||||
|
||||
template <> void Statement::bind(int offset, int64_t value) {
|
||||
BIND_3(int64, value)
|
||||
BIND_3(int64, value, stmt)
|
||||
}
|
||||
|
||||
template <> void Statement::bind(int offset, double value) {
|
||||
BIND_3(double, value)
|
||||
BIND_3(double, value, stmt)
|
||||
}
|
||||
|
||||
template <> void Statement::bind(int offset, bool value) {
|
||||
BIND_3(int, value)
|
||||
BIND_3(int, value, stmt)
|
||||
}
|
||||
|
||||
template <> void Statement::bind(int offset, const char *value) {
|
||||
BIND_5(text, value, -1, nullptr)
|
||||
BIND_5(text, value, -1, nullptr, stmt)
|
||||
}
|
||||
|
||||
void Statement::bind(int offset, const std::string &value, bool retain) {
|
||||
BIND_5(blob, value.data(), int(value.size()), retain ? SQLITE_TRANSIENT : SQLITE_STATIC)
|
||||
BIND_5(blob, value.data(), int(value.size()), retain ? SQLITE_TRANSIENT : SQLITE_STATIC, stmt)
|
||||
}
|
||||
|
||||
bool Statement::run() {
|
||||
@@ -143,7 +147,8 @@ bool Statement::run() {
|
||||
} else if (err == SQLITE_ROW) {
|
||||
return true;
|
||||
} else {
|
||||
throw std::runtime_error("failed to run statement");
|
||||
CHECK_SQLITE_OK_STMT(err, stmt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#include <mbgl/util/async_queue.hpp>
|
||||
#include <mbgl/util/variant.hpp>
|
||||
#include <mbgl/util/compression.hpp>
|
||||
#include <mbgl/util/io.hpp>
|
||||
#include <mbgl/platform/log.hpp>
|
||||
|
||||
#include "sqlite3.hpp"
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <uv.h>
|
||||
|
||||
@@ -88,10 +90,10 @@ struct SQLiteCache::ActionDispatcher {
|
||||
template <typename T> void operator()(T &t) { cache.process(t); }
|
||||
};
|
||||
|
||||
SQLiteCache::SQLiteCache(const std::string &path_)
|
||||
SQLiteCache::SQLiteCache(const std::string& path_)
|
||||
: path(path_),
|
||||
loop(uv_loop_new()),
|
||||
queue(new Queue(loop, [this](Action &action) {
|
||||
queue(new Queue(loop, [this](Action& action) {
|
||||
mapbox::util::apply_visitor(ActionDispatcher{ *this }, action);
|
||||
})),
|
||||
thread([this]() {
|
||||
@@ -99,8 +101,16 @@ SQLiteCache::SQLiteCache(const std::string &path_)
|
||||
pthread_setname_np("SQLite Cache");
|
||||
#endif
|
||||
uv_run(loop, UV_RUN_DEFAULT);
|
||||
})
|
||||
{
|
||||
|
||||
try {
|
||||
getStmt.reset();
|
||||
putStmt.reset();
|
||||
refreshStmt.reset();
|
||||
db.reset();
|
||||
} catch (mapbox::sqlite::Exception& ex) {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
}
|
||||
}) {
|
||||
}
|
||||
|
||||
SQLiteCache::~SQLiteCache() {
|
||||
@@ -140,6 +150,10 @@ void SQLiteCache::put(const Resource &resource, std::shared_ptr<const Response>
|
||||
void SQLiteCache::createDatabase() {
|
||||
db = util::make_unique<Database>(path.c_str(), ReadWrite | Create);
|
||||
|
||||
createSchema();
|
||||
}
|
||||
|
||||
void SQLiteCache::createSchema() {
|
||||
constexpr const char *const sql = ""
|
||||
"CREATE TABLE IF NOT EXISTS `http_cache` ("
|
||||
" `url` TEXT PRIMARY KEY NOT NULL,"
|
||||
@@ -155,111 +169,146 @@ void SQLiteCache::createDatabase() {
|
||||
|
||||
try {
|
||||
db->exec(sql);
|
||||
} catch(mapbox::sqlite::Exception &) {
|
||||
schema = true;
|
||||
} catch (mapbox::sqlite::Exception &ex) {
|
||||
|
||||
if (ex.code == SQLITE_NOTADB) {
|
||||
Log::Warning(Event::Database, "Trashing invalid database");
|
||||
db.reset();
|
||||
try {
|
||||
util::deleteFile(path);
|
||||
} catch (util::IOException& ioEx) {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
}
|
||||
db = util::make_unique<Database>(path.c_str(), ReadWrite | Create);
|
||||
} else {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
}
|
||||
|
||||
// Creating the database table + index failed. That means there may already be one, likely
|
||||
// with different columsn. Drop it and try to create a new one.
|
||||
try {
|
||||
db->exec("DROP TABLE IF EXISTS `http_cache`");
|
||||
db->exec(sql);
|
||||
} catch (mapbox::sqlite::Exception &ex) {
|
||||
Log::Error(Event::Database, "Failed to create database: %s", ex.what());
|
||||
db.reset();
|
||||
}
|
||||
db->exec("DROP TABLE IF EXISTS `http_cache`");
|
||||
db->exec(sql);
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteCache::process(GetAction &action) {
|
||||
// This is called in the SQLite event loop.
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
|
||||
if (!getStmt) {
|
||||
// Initialize the statement 0 1
|
||||
getStmt = util::make_unique<Statement>(db->prepare("SELECT `status`, `modified`, "
|
||||
// 2 3 4 5 1
|
||||
"`etag`, `expires`, `data`, `compressed` FROM `http_cache` WHERE `url` = ?"));
|
||||
} else {
|
||||
getStmt->reset();
|
||||
}
|
||||
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
getStmt->bind(1, unifiedURL.c_str());
|
||||
if (getStmt->run()) {
|
||||
// There is data.
|
||||
auto response = util::make_unique<Response>();
|
||||
response->status = Response::Status(getStmt->get<int>(0));
|
||||
response->modified = getStmt->get<int64_t>(1);
|
||||
response->etag = getStmt->get<std::string>(2);
|
||||
response->expires = getStmt->get<int64_t>(3);
|
||||
response->data = getStmt->get<std::string>(4);
|
||||
if (getStmt->get<int>(5)) { // == compressed
|
||||
response->data = util::decompress(response->data);
|
||||
try {
|
||||
// This is called in the SQLite event loop.
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
action.callback(std::move(response));
|
||||
} else {
|
||||
// There is no data.
|
||||
|
||||
if (!schema) {
|
||||
createSchema();
|
||||
}
|
||||
|
||||
if (!getStmt) {
|
||||
// Initialize the statement 0 1
|
||||
getStmt = util::make_unique<Statement>(db->prepare("SELECT `status`, `modified`, "
|
||||
// 2 3 4 5 1
|
||||
"`etag`, `expires`, `data`, `compressed` FROM `http_cache` WHERE `url` = ?"));
|
||||
} else {
|
||||
getStmt->reset();
|
||||
}
|
||||
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
getStmt->bind(1, unifiedURL.c_str());
|
||||
if (getStmt->run()) {
|
||||
// There is data.
|
||||
auto response = util::make_unique<Response>();
|
||||
response->status = Response::Status(getStmt->get<int>(0));
|
||||
response->modified = getStmt->get<int64_t>(1);
|
||||
response->etag = getStmt->get<std::string>(2);
|
||||
response->expires = getStmt->get<int64_t>(3);
|
||||
response->data = getStmt->get<std::string>(4);
|
||||
if (getStmt->get<int>(5)) { // == compressed
|
||||
response->data = util::decompress(response->data);
|
||||
}
|
||||
action.callback(std::move(response));
|
||||
} else {
|
||||
// There is no data.
|
||||
action.callback(nullptr);
|
||||
}
|
||||
} catch (mapbox::sqlite::Exception& ex) {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
action.callback(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteCache::process(PutAction &action) {
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
try {
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
|
||||
if (!putStmt) {
|
||||
putStmt = util::make_unique<Statement>(db->prepare("REPLACE INTO `http_cache` ("
|
||||
// 1 2 3 4 5 6 7 8
|
||||
"`url`, `status`, `kind`, `modified`, `etag`, `expires`, `data`, `compressed`"
|
||||
") VALUES(?, ?, ?, ?, ?, ?, ?, ?)"));
|
||||
} else {
|
||||
putStmt->reset();
|
||||
}
|
||||
if (!schema) {
|
||||
createSchema();
|
||||
}
|
||||
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
putStmt->bind(1 /* url */, unifiedURL.c_str());
|
||||
putStmt->bind(2 /* status */, int(action.response->status));
|
||||
putStmt->bind(3 /* kind */, int(action.resource.kind));
|
||||
putStmt->bind(4 /* modified */, action.response->modified);
|
||||
putStmt->bind(5 /* etag */, action.response->etag.c_str());
|
||||
putStmt->bind(6 /* expires */, action.response->expires);
|
||||
if (!putStmt) {
|
||||
putStmt = util::make_unique<Statement>(db->prepare("REPLACE INTO `http_cache` ("
|
||||
// 1 2 3 4 5 6 7 8
|
||||
"`url`, `status`, `kind`, `modified`, `etag`, `expires`, `data`, `compressed`"
|
||||
") VALUES(?, ?, ?, ?, ?, ?, ?, ?)"));
|
||||
} else {
|
||||
putStmt->reset();
|
||||
}
|
||||
|
||||
std::string data;
|
||||
if (action.resource.kind != Resource::Image) {
|
||||
// Do not compress images, since they are typically compressed already.
|
||||
data = util::compress(action.response->data);
|
||||
}
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
putStmt->bind(1 /* url */, unifiedURL.c_str());
|
||||
putStmt->bind(2 /* status */, int(action.response->status));
|
||||
putStmt->bind(3 /* kind */, int(action.resource.kind));
|
||||
putStmt->bind(4 /* modified */, action.response->modified);
|
||||
putStmt->bind(5 /* etag */, action.response->etag.c_str());
|
||||
putStmt->bind(6 /* expires */, action.response->expires);
|
||||
|
||||
if (!data.empty() && data.size() < action.response->data.size()) {
|
||||
// Store the compressed data when it is smaller than the original
|
||||
// uncompressed data.
|
||||
putStmt->bind(7 /* data */, data, false); // do not retain the string internally.
|
||||
putStmt->bind(8 /* compressed */, true);
|
||||
} else {
|
||||
putStmt->bind(7 /* data */, action.response->data, false); // do not retain the string internally.
|
||||
putStmt->bind(8 /* compressed */, false);
|
||||
}
|
||||
std::string data;
|
||||
if (action.resource.kind != Resource::Image) {
|
||||
// Do not compress images, since they are typically compressed already.
|
||||
data = util::compress(action.response->data);
|
||||
}
|
||||
|
||||
putStmt->run();
|
||||
if (!data.empty() && data.size() < action.response->data.size()) {
|
||||
// Store the compressed data when it is smaller than the original
|
||||
// uncompressed data.
|
||||
putStmt->bind(7 /* data */, data, false); // do not retain the string internally.
|
||||
putStmt->bind(8 /* compressed */, true);
|
||||
} else {
|
||||
putStmt->bind(7 /* data */, action.response->data, false); // do not retain the string internally.
|
||||
putStmt->bind(8 /* compressed */, false);
|
||||
}
|
||||
|
||||
putStmt->run();
|
||||
} catch (mapbox::sqlite::Exception& ex) {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteCache::process(RefreshAction &action) {
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
try {
|
||||
if (!db) {
|
||||
createDatabase();
|
||||
}
|
||||
|
||||
if (!refreshStmt) {
|
||||
refreshStmt = util::make_unique<Statement>( // 1 2
|
||||
db->prepare("UPDATE `http_cache` SET `expires` = ? WHERE `url` = ?"));
|
||||
} else {
|
||||
refreshStmt->reset();
|
||||
}
|
||||
if (!schema) {
|
||||
createSchema();
|
||||
}
|
||||
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
refreshStmt->bind(1, int64_t(action.expires));
|
||||
refreshStmt->bind(2, unifiedURL.c_str());
|
||||
refreshStmt->run();
|
||||
if (!refreshStmt) {
|
||||
refreshStmt = util::make_unique<Statement>( // 1 2
|
||||
db->prepare("UPDATE `http_cache` SET `expires` = ? WHERE `url` = ?"));
|
||||
} else {
|
||||
refreshStmt->reset();
|
||||
}
|
||||
|
||||
const std::string unifiedURL = unifyMapboxURLs(action.resource.url);
|
||||
refreshStmt->bind(1, int64_t(action.expires));
|
||||
refreshStmt->bind(2, unifiedURL.c_str());
|
||||
refreshStmt->run();
|
||||
} catch (mapbox::sqlite::Exception& ex) {
|
||||
Log::Error(Event::Database, ex.code, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteCache::process(StopAction &) {
|
||||
|
||||
@@ -17,6 +17,12 @@ void Log::setObserver(std::unique_ptr<Observer> observer) {
|
||||
currentObserver = std::move(observer);
|
||||
}
|
||||
|
||||
std::unique_ptr<Log::Observer> Log::removeObserver() {
|
||||
std::unique_ptr<Observer> observer;
|
||||
std::swap(observer, currentObserver);
|
||||
return observer;
|
||||
}
|
||||
|
||||
void Log::record(EventSeverity severity, Event event, const std::string &msg) {
|
||||
record(severity, event, -1, msg);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
namespace mbgl {
|
||||
namespace util {
|
||||
@@ -30,5 +31,12 @@ std::string read_file(const std::string &filename) {
|
||||
}
|
||||
}
|
||||
|
||||
void deleteFile(const std::string& filename) {
|
||||
const int ret = unlink(filename.c_str());
|
||||
if (ret == -1) {
|
||||
throw IOException(errno, "failed to unlink file");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
#define MBGL_UTIL_IO
|
||||
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace mbgl {
|
||||
namespace util {
|
||||
|
||||
struct IOException : std::runtime_error {
|
||||
inline IOException(int err, const char* msg) : std::runtime_error(msg), code(err) {
|
||||
}
|
||||
const int code = 0;
|
||||
};
|
||||
|
||||
void write_file(const std::string &filename, const std::string &data);
|
||||
std::string read_file(const std::string &filename);
|
||||
|
||||
void deleteFile(const std::string& filename);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
test/fixtures/fixture_log_observer.cpp
vendored
3
test/fixtures/fixture_log_observer.cpp
vendored
@@ -8,6 +8,9 @@ FixtureLogObserver::LogMessage::LogMessage(EventSeverity severity_, Event event_
|
||||
: severity(severity_), event(event_), code(code_), msg(msg_) {
|
||||
}
|
||||
|
||||
FixtureLogObserver::LogMessage::LogMessage() : severity(), event(), code(), msg() {
|
||||
}
|
||||
|
||||
bool FixtureLogObserver::LogMessage::operator==(const LogMessage &rhs) const {
|
||||
return (!severity || !rhs.severity || severity.get() == rhs.severity.get()) &&
|
||||
(!event || !rhs.event || event.get() == rhs.event.get()) &&
|
||||
|
||||
4
test/fixtures/fixture_log_observer.hpp
vendored
4
test/fixtures/fixture_log_observer.hpp
vendored
@@ -2,7 +2,6 @@
|
||||
#define MBGL_TEST_FIXTURE_LOG_OBSERVER
|
||||
|
||||
#include <mbgl/platform/log.hpp>
|
||||
#include <mbgl/util/noncopyable.hpp>
|
||||
#include <mbgl/util/optional.hpp>
|
||||
|
||||
#include <vector>
|
||||
@@ -11,10 +10,11 @@
|
||||
|
||||
namespace mbgl {
|
||||
|
||||
class FixtureLogObserver : public Log::Observer, private util::noncopyable {
|
||||
class FixtureLogObserver : public Log::Observer {
|
||||
public:
|
||||
struct LogMessage {
|
||||
LogMessage(EventSeverity severity_, Event event_, int64_t code_, const std::string &msg_);
|
||||
LogMessage();
|
||||
|
||||
bool operator==(const LogMessage &rhs) const;
|
||||
|
||||
|
||||
391
test/storage/database.cpp
Normal file
391
test/storage/database.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
#include "storage.hpp"
|
||||
#include "../fixtures/fixture_log_observer.hpp"
|
||||
|
||||
#include <future>
|
||||
|
||||
#include <mbgl/storage/default/sqlite_cache.hpp>
|
||||
#include <mbgl/storage/resource.hpp>
|
||||
#include <mbgl/storage/response.hpp>
|
||||
#include <mbgl/util/io.hpp>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
class ScopedTest {
|
||||
public:
|
||||
ScopedTest(std::function<void()> destructor_) : destructor(destructor_) {}
|
||||
ScopedTest() {}
|
||||
|
||||
void finish() {
|
||||
promise.set_value();
|
||||
}
|
||||
|
||||
~ScopedTest() {
|
||||
promise.get_future().get();
|
||||
if (destructor) {
|
||||
destructor();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<void()> destructor;
|
||||
std::promise<void> promise;
|
||||
};
|
||||
|
||||
TEST_F(Storage, DatabaseDoesNotExist) {
|
||||
using namespace mbgl;
|
||||
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
|
||||
ScopedTest test([&] {
|
||||
auto observer = Log::removeObserver();
|
||||
EXPECT_EQ(1ul, dynamic_cast<FixtureLogObserver*>(observer.get())->count({ EventSeverity::Error, Event::Database, 14, "unable to open database file" }));
|
||||
});
|
||||
|
||||
SQLiteCache cache("test/fixtures/404/cache.db");
|
||||
|
||||
cache.get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
test.finish();
|
||||
});
|
||||
}
|
||||
|
||||
void createDir(const char* name) {
|
||||
const int ret = mkdir(name, 0755);
|
||||
if (ret == -1) {
|
||||
ASSERT_EQ(EEXIST, errno);
|
||||
} else {
|
||||
ASSERT_EQ(0, ret);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteFile(const char* name) {
|
||||
const int ret = unlink(name);
|
||||
if (ret == -1) {
|
||||
ASSERT_EQ(ENOENT, errno);
|
||||
} else {
|
||||
ASSERT_EQ(0, ret);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void writeFile(const char* name, const std::string& data) {
|
||||
mbgl::util::write_file(name, data);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(Storage, DatabaseCreate) {
|
||||
using namespace mbgl;
|
||||
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/cache.db");
|
||||
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
|
||||
ScopedTest test([&] {
|
||||
Log::removeObserver();
|
||||
});
|
||||
|
||||
SQLiteCache cache("test/fixtures/database/cache.db");
|
||||
|
||||
cache.get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
test.finish();
|
||||
});
|
||||
}
|
||||
|
||||
class FileLock {
|
||||
public:
|
||||
FileLock(const std::string& path) {
|
||||
const int err = sqlite3_open_v2(path.c_str(), &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr);
|
||||
if (err != SQLITE_OK) {
|
||||
throw std::runtime_error("Could not open db");
|
||||
}
|
||||
lock();
|
||||
}
|
||||
|
||||
void lock() {
|
||||
assert(!locked);
|
||||
const int err = sqlite3_exec(db, "begin exclusive transaction", nullptr, nullptr, nullptr);
|
||||
if (err != SQLITE_OK) {
|
||||
throw std::runtime_error("Could not lock db");
|
||||
}
|
||||
locked = true;
|
||||
}
|
||||
|
||||
void unlock() {
|
||||
assert(locked);
|
||||
const int err = sqlite3_exec(db, "commit", nullptr, nullptr, nullptr);
|
||||
if (err != SQLITE_OK) {
|
||||
throw std::runtime_error("Could not unlock db");
|
||||
}
|
||||
locked = false;
|
||||
}
|
||||
|
||||
~FileLock() {
|
||||
if (locked) {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3* db;
|
||||
bool locked = false;
|
||||
};
|
||||
|
||||
TEST_F(Storage, DatabaseLockedRead) {
|
||||
using namespace mbgl;
|
||||
|
||||
// Create a locked file.
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/locked.db");
|
||||
FileLock guard("test/fixtures/database/locked.db");
|
||||
|
||||
auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
{
|
||||
// First request should fail.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
// Make sure that we got a few "database locked" errors
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(2ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
|
||||
}
|
||||
|
||||
// Then, unlock the file and try again.
|
||||
guard.unlock();
|
||||
|
||||
{
|
||||
// First, try getting a file (the cache value should not exist).
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
// Make sure that we got a no errors
|
||||
Log::removeObserver();
|
||||
}
|
||||
|
||||
// Explicitly delete the Cache now.
|
||||
cache.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_F(Storage, DatabaseLockedWrite) {
|
||||
using namespace mbgl;
|
||||
|
||||
// Create a locked file.
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/locked.db");
|
||||
FileLock guard("test/fixtures/database/locked.db");
|
||||
|
||||
auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
{
|
||||
// Adds a file (which should fail).
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
auto response = std::make_shared<Response>();
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
|
||||
}
|
||||
|
||||
// Then, unlock the file and try again.
|
||||
guard.unlock();
|
||||
|
||||
{
|
||||
// Then, set a file and obtain it again.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_NE(nullptr, res.get());
|
||||
EXPECT_EQ("Demo", res->data);
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
// Make sure that we got a no errors
|
||||
Log::removeObserver();
|
||||
}
|
||||
|
||||
// Explicitly delete the Cache now.
|
||||
cache.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
TEST_F(Storage, DatabaseLockedRefresh) {
|
||||
using namespace mbgl;
|
||||
|
||||
// Create a locked file.
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/locked.db");
|
||||
|
||||
auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
|
||||
|
||||
// Then, lock the file and try again.
|
||||
FileLock guard("test/fixtures/database/locked.db");
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
{
|
||||
// Adds a file.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
|
||||
}
|
||||
|
||||
{
|
||||
// Then, try to refresh it.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Refresh);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_EQ(nullptr, res.get());
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
// Make sure that we got the right errors.
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(4ul, flo->count({ EventSeverity::Error, Event::Database, 5, "database is locked" }));
|
||||
}
|
||||
|
||||
// Explicitly delete the Cache now.
|
||||
cache.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_F(Storage, DatabaseDeleted) {
|
||||
using namespace mbgl;
|
||||
|
||||
// Create a locked file.
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/locked.db");
|
||||
|
||||
auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/locked.db");
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
{
|
||||
// Adds a file.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_NE(nullptr, res.get());
|
||||
EXPECT_EQ("Demo", res->data);
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
Log::removeObserver();
|
||||
}
|
||||
|
||||
deleteFile("test/fixtures/database/locked.db");
|
||||
|
||||
{
|
||||
// Adds a file.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_NE(nullptr, res.get());
|
||||
EXPECT_EQ("Demo", res->data);
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(1ul, flo->count({ EventSeverity::Error, Event::Database, 8, "attempt to write a readonly database" }));
|
||||
}
|
||||
|
||||
// Explicitly delete the Cache now.
|
||||
cache.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_F(Storage, DatabaseInvalid) {
|
||||
using namespace mbgl;
|
||||
|
||||
// Create a locked file.
|
||||
createDir("test/fixtures/database");
|
||||
deleteFile("test/fixtures/database/invalid.db");
|
||||
writeFile("test/fixtures/database/invalid.db", "this is an invalid file");
|
||||
|
||||
auto cache = util::make_unique<SQLiteCache>("test/fixtures/database/invalid.db");
|
||||
|
||||
std::promise<void> promise;
|
||||
|
||||
{
|
||||
// Adds a file.
|
||||
Log::setObserver(util::make_unique<FixtureLogObserver>());
|
||||
promise = {};
|
||||
auto response = std::make_shared<Response>();
|
||||
response->data = "Demo";
|
||||
cache->put({ Resource::Unknown, "mapbox://test" }, response, FileCache::Hint::Full);
|
||||
cache->get({ Resource::Unknown, "mapbox://test" }, [&] (std::unique_ptr<Response> res) {
|
||||
EXPECT_NE(nullptr, res.get());
|
||||
EXPECT_EQ("Demo", res->data);
|
||||
promise.set_value();
|
||||
});
|
||||
promise.get_future().get();
|
||||
|
||||
auto observer = Log::removeObserver();
|
||||
auto flo = dynamic_cast<FixtureLogObserver*>(observer.get());
|
||||
EXPECT_EQ(1ul, flo->count({ EventSeverity::Warning, Event::Database, -1, "Trashing invalid database" }));
|
||||
}
|
||||
|
||||
// Explicitly delete the Cache now.
|
||||
cache.reset();
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
'storage/storage.cpp',
|
||||
'storage/cache_response.cpp',
|
||||
'storage/cache_revalidate.cpp',
|
||||
'storage/database.cpp',
|
||||
'storage/directory_reading.cpp',
|
||||
'storage/file_reading.cpp',
|
||||
'storage/http_cancel.cpp',
|
||||
|
||||
Reference in New Issue
Block a user