ResourceCache
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <sqlite3.h>
|
||||
#include <TOSLib.hpp>
|
||||
#include <TOSAsync.hpp>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
namespace LV::Client {
|
||||
|
||||
using namespace TOS;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// NOT ThreadSafe
|
||||
class ResourceCacheHandler {
|
||||
class CacheDatabase {
|
||||
const fs::path Path;
|
||||
|
||||
sqlite3 *DB = nullptr;
|
||||
@@ -21,291 +25,144 @@ class ResourceCacheHandler {
|
||||
*STMT_REMOVE = nullptr,
|
||||
*STMT_ALL_HASH = nullptr,
|
||||
*STMT_SUM = nullptr,
|
||||
*STMT_OLD = nullptr,
|
||||
*STMT_TO_FREE = nullptr,
|
||||
*STMT_COUNT = nullptr;
|
||||
|
||||
size_t Size = -1;
|
||||
|
||||
public:
|
||||
ResourceCacheHandler(const std::string_view cache_path)
|
||||
: Path(cache_path)
|
||||
{
|
||||
int errc = sqlite3_open_v2((Path / "db.sqlite3").c_str(), &DB, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr);
|
||||
if(errc) {
|
||||
MAKE_ERROR("Не удалось открыть базу данных " << (Path / "db.sqlite3").c_str() << ": " << sqlite3_errmsg(DB));
|
||||
}
|
||||
CacheDatabase(const fs::path &cachePath);
|
||||
~CacheDatabase();
|
||||
|
||||
const char* sql = R"(
|
||||
CREATE TABLE IF NOT EXISTS files(
|
||||
sha256 BLOB(32) NOT NULL, --
|
||||
last_used INT NOT NULL, -- unix timestamp
|
||||
size INT NOT NULL, -- file size
|
||||
UNIQUE (sha256));
|
||||
)";
|
||||
|
||||
errc = sqlite3_exec(DB, sql, nullptr, nullptr, nullptr);
|
||||
if(errc != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить таблицу базы: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
INSERT OR REPLACE INTO files (sha256, last_used, size)
|
||||
VALUES (?, ?, ?);
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_INSERT, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_INSERT: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
UPDATE files SET last_used = ? WHERE sha256 = ?;
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_UPDATE_TIME, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_UPDATE_TIME: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
DELETE FROM files WHERE sha256=?;
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_REMOVE, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_REMOVE: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
SELECT sha256 FROM files;
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_ALL_HASH, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_ALL_HASH: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
SELECT SUM(size) FROM files;
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_SUM, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_SUM: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
SELECT sha256
|
||||
FROM files
|
||||
WHERE last_used < ?
|
||||
ORDER BY last_used ASC, size ASC
|
||||
LIMIT (
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT SUM(size) OVER (ORDER BY last_used ASC, size ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
|
||||
FROM files
|
||||
WHERE last_used < ?
|
||||
ORDER BY last_used ASC, size ASC
|
||||
) sub
|
||||
WHERE running_total <= ?
|
||||
);
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_TO_FREE, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_TO_FREE: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sql = R"(
|
||||
SELECT COUNT(*) FROM files;
|
||||
)";
|
||||
|
||||
if(sqlite3_prepare_v2(DB, sql, -1, &STMT_COUNT, nullptr) != SQLITE_OK) {
|
||||
MAKE_ERROR("Не удалось подготовить запрос STMT_COUNT: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
}
|
||||
|
||||
~ResourceCacheHandler() {
|
||||
for(sqlite3_stmt* stmt : {STMT_INSERT, STMT_UPDATE_TIME, STMT_REMOVE, STMT_ALL_HASH, STMT_SUM, STMT_TO_FREE, STMT_COUNT})
|
||||
if(stmt)
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if(DB)
|
||||
sqlite3_close(DB);
|
||||
}
|
||||
|
||||
ResourceCacheHandler(const ResourceCacheHandler&) = delete;
|
||||
ResourceCacheHandler(ResourceCacheHandler&&) = delete;
|
||||
ResourceCacheHandler& operator=(const ResourceCacheHandler&) = delete;
|
||||
ResourceCacheHandler& operator=(ResourceCacheHandler&&) = delete;
|
||||
CacheDatabase(const CacheDatabase&) = delete;
|
||||
CacheDatabase(CacheDatabase&&) = delete;
|
||||
CacheDatabase& operator=(const CacheDatabase&) = delete;
|
||||
CacheDatabase& operator=(CacheDatabase&&) = delete;
|
||||
|
||||
/*
|
||||
Выдаёт размер занимаемый всем хранимым кешем
|
||||
*/
|
||||
size_t getCacheSize() {
|
||||
if(Size == -1) {
|
||||
if(sqlite3_step(STMT_SUM) != SQLITE_ROW) {
|
||||
sqlite3_reset(STMT_SUM);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_SUM: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
Size = sqlite3_column_int(STMT_SUM, 0);
|
||||
sqlite3_reset(STMT_SUM);
|
||||
}
|
||||
|
||||
return Size;
|
||||
}
|
||||
size_t getCacheSize();
|
||||
|
||||
// TODO: добавить ограничения на количество файлов
|
||||
|
||||
/*
|
||||
Создаёт линейный массив в котором подряд указаны все хэш суммы в бинарном виде и возвращает их количество
|
||||
*/
|
||||
std::pair<std::string, size_t> getAllHash() {
|
||||
if(sqlite3_step(STMT_COUNT) != SQLITE_ROW) {
|
||||
sqlite3_reset(STMT_COUNT);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_COUNT: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
size_t count = sqlite3_column_int(STMT_COUNT, 0);
|
||||
sqlite3_reset(STMT_COUNT);
|
||||
|
||||
std::string out;
|
||||
out.reserve(32*count);
|
||||
|
||||
int errc;
|
||||
size_t readed = 0;
|
||||
while(true) {
|
||||
errc = sqlite3_step(STMT_ALL_HASH);
|
||||
if(errc == SQLITE_DONE)
|
||||
break;
|
||||
else if(errc != SQLITE_ROW) {
|
||||
sqlite3_reset(STMT_ALL_HASH);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_ALL_HASH: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
const char *hash = (const char*) sqlite3_column_blob(STMT_ALL_HASH, 0);
|
||||
readed++;
|
||||
out += std::string_view(hash, hash+32);
|
||||
}
|
||||
|
||||
sqlite3_reset(STMT_ALL_HASH);
|
||||
return {out, readed};
|
||||
}
|
||||
std::pair<std::string, size_t> getAllHash();
|
||||
|
||||
using HASH = std::array<uint8_t, 32>;
|
||||
|
||||
/*
|
||||
Обновляет время использования кеша
|
||||
*/
|
||||
void updateTimeFor(HASH hash) {
|
||||
sqlite3_bind_blob(STMT_UPDATE_TIME, 0, (const void*) hash.data(), 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int(STMT_UPDATE_TIME, 1, time(nullptr));
|
||||
if(sqlite3_step(STMT_UPDATE_TIME) != SQLITE_OK) {
|
||||
sqlite3_reset(STMT_UPDATE_TIME);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_UPDATE_TIME: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sqlite3_reset(STMT_UPDATE_TIME);
|
||||
}
|
||||
void updateTimeFor(HASH hash);
|
||||
|
||||
/*
|
||||
Добавляет запись
|
||||
*/
|
||||
void insert(HASH hash, size_t size) {
|
||||
assert(size < (size_t(1) << 31)-1 && size > 0);
|
||||
|
||||
sqlite3_bind_blob(STMT_INSERT, 0, (const void*) hash.data(), 32, SQLITE_STATIC);
|
||||
sqlite3_bind_int(STMT_INSERT, 1, (int) size);
|
||||
sqlite3_bind_int(STMT_INSERT, 2, time(nullptr));
|
||||
if(sqlite3_step(STMT_INSERT) != SQLITE_OK) {
|
||||
sqlite3_reset(STMT_INSERT);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_INSERT: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
sqlite3_reset(STMT_INSERT);
|
||||
}
|
||||
void insert(HASH hash, size_t size);
|
||||
|
||||
/*
|
||||
Выдаёт хэши на удаление по размеру в сумме больше bytesToFree. В приоритете старые, потом мелкие
|
||||
Выдаёт хэши на удаление по размеру в сумме больше bytesToFree.
|
||||
Сначала удаляется старьё, потом по приоритету дата использования + размер
|
||||
*/
|
||||
std::vector<HASH> findExcessHashes(size_t bytesToFree, int timeBefore = time(nullptr)-604800) {
|
||||
sqlite3_bind_int(STMT_TO_FREE, 0, timeBefore);
|
||||
sqlite3_bind_int(STMT_TO_FREE, 1, timeBefore);
|
||||
sqlite3_bind_int(STMT_TO_FREE, 2, (int) bytesToFree);
|
||||
|
||||
std::vector<HASH> out;
|
||||
while(true) {
|
||||
int errc = sqlite3_step(STMT_TO_FREE);
|
||||
if(errc == SQLITE_DONE)
|
||||
break;
|
||||
else if(errc != SQLITE_ROW) {
|
||||
sqlite3_reset(STMT_TO_FREE);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_TO_FREE: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
|
||||
const uint8_t *hash = (const uint8_t*) sqlite3_column_blob(STMT_TO_FREE, 0);
|
||||
HASH obj;
|
||||
for(int iter = 0; iter < 32; iter++)
|
||||
obj[iter] = hash[iter];
|
||||
|
||||
out.push_back(obj);
|
||||
}
|
||||
|
||||
sqlite3_reset(STMT_TO_FREE);
|
||||
return out;
|
||||
}
|
||||
std::vector<HASH> findExcessHashes(size_t bytesToFree, int timeBefore);
|
||||
|
||||
/*
|
||||
Удаление записи
|
||||
*/
|
||||
void remove(HASH hash) {
|
||||
sqlite3_bind_blob(STMT_REMOVE, 0, (const void*) hash.data(), 32, SQLITE_STATIC);
|
||||
if(sqlite3_step(STMT_REMOVE) != SQLITE_OK) {
|
||||
sqlite3_reset(STMT_REMOVE);
|
||||
MAKE_ERROR("Не удалось выполнить подготовленный запрос STMT_REMOVE: " << sqlite3_errmsg(DB));
|
||||
}
|
||||
void remove(HASH hash);
|
||||
|
||||
sqlite3_reset(STMT_REMOVE);
|
||||
}
|
||||
|
||||
static std::string hashToString(HASH hash) {
|
||||
std::string text;
|
||||
text.reserve(64);
|
||||
|
||||
for(int iter = 0; iter < 32; iter++) {
|
||||
int val = hash[31-iter] & 0xf;
|
||||
if(val > 9)
|
||||
text += 'a'+val-10;
|
||||
else
|
||||
text += '0'+val;
|
||||
|
||||
val = (hash[31-iter] >> 4) & 0xf;
|
||||
if(val > 9)
|
||||
text += 'a'+val-10;
|
||||
else
|
||||
text += '0'+val;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static int hexCharToInt(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
throw std::invalid_argument("Invalid hexadecimal character");
|
||||
}
|
||||
|
||||
static HASH stringToHash(const std::string_view view) {
|
||||
if (view.size() != 64)
|
||||
throw std::invalid_argument("Hex string must be exactly 64 characters long");
|
||||
|
||||
HASH hash;
|
||||
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
size_t offset = 62 - i * 2;
|
||||
int high = hexCharToInt(view[offset]);
|
||||
int low = hexCharToInt(view[offset + 1]);
|
||||
hash[i] = (high << 4) | low;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
static std::string hashToString(HASH hash);
|
||||
static int hexCharToInt(char c);
|
||||
static HASH stringToHash(const std::string_view view);
|
||||
};
|
||||
|
||||
/*
|
||||
Читает и пишет ресурсы на диск
|
||||
В приоритете чтение
|
||||
|
||||
Кодировки только на стороне сервера, на клиенте уже готовые данные
|
||||
|
||||
NOT ThreadSafe
|
||||
*/
|
||||
class CacheHandler : public IAsyncDestructible {
|
||||
protected:
|
||||
const fs::path Path;
|
||||
CacheDatabase DB;
|
||||
|
||||
protected:
|
||||
CacheHandler(boost::asio::io_context &ioc, const fs::path &cachePath);
|
||||
|
||||
public:
|
||||
virtual ~CacheHandler();
|
||||
|
||||
// Добавить задачу на запись
|
||||
virtual void pushWrite(std::string &&data, CacheDatabase::HASH hash) = 0;
|
||||
|
||||
// Добавить задачу на чтение
|
||||
virtual void pushRead(CacheDatabase::HASH hash) = 0;
|
||||
|
||||
// Получить считанные данные
|
||||
virtual std::vector<std::pair<CacheDatabase::HASH, std::string>> pullReads() = 0;
|
||||
|
||||
// Получить список доступных ресурсов
|
||||
std::pair<std::string, size_t> getAll();
|
||||
};
|
||||
|
||||
class CacheHandlerBasic : public CacheHandler {
|
||||
Logger LOG = "CacheHandlerBasic";
|
||||
|
||||
std::thread ReadThread, ReadWriteThread;
|
||||
|
||||
struct DataTask {
|
||||
CacheDatabase::HASH Hash;
|
||||
std::shared_ptr<std::string> Data;
|
||||
};
|
||||
|
||||
// Очередь задач на чтение
|
||||
SpinlockObject<std::queue<CacheDatabase::HASH>> ReadQueue;
|
||||
// Кэш данных, которые ещё не записались
|
||||
SpinlockObject<std::vector<std::pair<CacheDatabase::HASH, std::shared_ptr<std::string>>>> WriteCache;
|
||||
// Очередь записи данных на диск
|
||||
SpinlockObject<std::queue<DataTask>> WriteQueue;
|
||||
// Список полностью считанных файлов
|
||||
SpinlockObject<std::vector<DataTask>> ReadedQueue;
|
||||
bool NeedShutdown = false;
|
||||
size_t MaxCacheDirectorySize = 8*1024*1024*1024ULL;
|
||||
size_t MaxLifeTime = 7*24*60*60;
|
||||
|
||||
public:
|
||||
using Ptr = std::shared_ptr<CacheHandlerBasic>;
|
||||
|
||||
private:
|
||||
virtual coro<> asyncDestructor() override;
|
||||
|
||||
void readThread(AsyncUseControl::Lock lock);
|
||||
|
||||
void readWriteThread(AsyncUseControl::Lock lock);
|
||||
|
||||
protected:
|
||||
CacheHandlerBasic(boost::asio::io_context &ioc, const fs::path& cachePath);
|
||||
|
||||
public:
|
||||
virtual ~CacheHandlerBasic();
|
||||
|
||||
static std::shared_ptr<CacheHandlerBasic> Create(asio::io_context &ioc, const fs::path& cachePath) {
|
||||
return createShared(ioc, new CacheHandlerBasic(ioc, cachePath));
|
||||
}
|
||||
|
||||
virtual void pushWrite(std::string &&data, CacheDatabase::HASH hash) override;
|
||||
virtual void pushRead(CacheDatabase::HASH hash) override;
|
||||
virtual std::vector<std::pair<CacheDatabase::HASH, std::string>> pullReads() override;
|
||||
};
|
||||
|
||||
#ifdef LUAVOX_HAVE_LIBURING
|
||||
|
||||
class CacheHandlerUring : public CacheHandler {
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user