Merge pull request #40543 from Lloyd-Pottiger/feat/support-read-only-for-embeddedrocksdb

Add read-only support for EmbeddedRocksDB
This commit is contained in:
Antonio Andelic 2022-09-05 09:31:07 +02:00 committed by GitHub
commit 3a0581e990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 206 additions and 9 deletions

View File

@ -16,12 +16,14 @@ CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE = EmbeddedRocksDB([ttl]) PRIMARY KEY(primary_key_name)
) ENGINE = EmbeddedRocksDB([ttl, rocksdb_dir, read_only]) PRIMARY KEY(primary_key_name)
```
Engine parameters:
- `ttl` - time to live for values. TTL is accepted in seconds. If TTL is 0, regular RocksDB instance is used (without TTL).
- `rocksdb_dir` - path to the directory of an existed RocksDB or the destination path of the created RocksDB. Open the table with the specified `rocksdb_dir`.
- `read_only` - when `read_only` is set to true, read-only mode is used. For storage with TTL, compaction will not be triggered (neither manual nor automatic), so no expired entries are removed.
- `primary_key_name` any column name in the column list.
- `primary key` must be specified, it supports only one column in the primary key. The primary key will be serialized in binary as a `rocksdb key`.
- columns other than the primary key will be serialized in binary as `rocksdb` value in corresponding order.

View File

@ -29,6 +29,7 @@
#include <cstddef>
#include <filesystem>
#include <shared_mutex>
#include <utility>
namespace fs = std::filesystem;
@ -166,14 +167,21 @@ StorageEmbeddedRocksDB::StorageEmbeddedRocksDB(const StorageID & table_id_,
bool attach,
ContextPtr context_,
const String & primary_key_,
Int32 ttl_)
Int32 ttl_,
String rocksdb_dir_,
bool read_only_)
: IStorage(table_id_)
, WithContext(context_->getGlobalContext())
, primary_key{primary_key_}
, rocksdb_dir(std::move(rocksdb_dir_))
, ttl(ttl_)
, read_only(read_only_)
{
setInMemoryMetadata(metadata_);
rocksdb_dir = context_->getPath() + relative_data_path_;
if (rocksdb_dir.empty())
{
rocksdb_dir = context_->getPath() + relative_data_path_;
}
if (!attach)
{
fs::create_directories(rocksdb_dir);
@ -269,7 +277,7 @@ void StorageEmbeddedRocksDB::initDB()
if (ttl > 0)
{
rocksdb::DBWithTTL * db;
status = rocksdb::DBWithTTL::Open(merged, rocksdb_dir, &db, ttl);
status = rocksdb::DBWithTTL::Open(merged, rocksdb_dir, &db, ttl, read_only);
if (!status.ok())
{
throw Exception(ErrorCodes::ROCKSDB_ERROR, "Failed to open rocksdb path at: {}: {}",
@ -280,7 +288,14 @@ void StorageEmbeddedRocksDB::initDB()
else
{
rocksdb::DB * db;
status = rocksdb::DB::Open(merged, rocksdb_dir, &db);
if (read_only)
{
status = rocksdb::DB::OpenForReadOnly(merged, rocksdb_dir, &db);
}
else
{
status = rocksdb::DB::Open(merged, rocksdb_dir, &db);
}
if (!status.ok())
{
throw Exception(ErrorCodes::ROCKSDB_ERROR, "Failed to open rocksdb path at: {}: {}",
@ -351,15 +366,21 @@ static StoragePtr create(const StorageFactory::Arguments & args)
{
// TODO custom RocksDBSettings, table function
auto engine_args = args.engine_args;
if (engine_args.size() > 1)
if (engine_args.size() > 3)
{
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH, "Engine {} requires at most 1 parameter. ({} given). Correct usage: EmbeddedRocksDB([ttl])",
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH, "Engine {} requires at most 3 parameters. ({} given). Correct usage: EmbeddedRocksDB([ttl, rocksdb_dir, read_only])",
args.engine_name, engine_args.size());
}
Int32 ttl{0};
String rocksdb_dir;
bool read_only{false};
if (!engine_args.empty())
ttl = checkAndGetLiteralArgument<UInt64>(engine_args[0], "ttl");
if (engine_args.size() > 1)
rocksdb_dir = checkAndGetLiteralArgument<String>(engine_args[1], "rocksdb_dir");
if (engine_args.size() > 2)
read_only = checkAndGetLiteralArgument<bool>(engine_args[2], "read_only");
StorageInMemoryMetadata metadata;
metadata.setColumns(args.columns);
@ -374,7 +395,7 @@ static StoragePtr create(const StorageFactory::Arguments & args)
{
throw Exception("StorageEmbeddedRocksDB must require one column in primary key", ErrorCodes::BAD_ARGUMENTS);
}
return std::make_shared<StorageEmbeddedRocksDB>(args.table_id, args.relative_data_path, metadata, args.attach, args.getContext(), primary_key_names[0], ttl);
return std::make_shared<StorageEmbeddedRocksDB>(args.table_id, args.relative_data_path, metadata, args.attach, args.getContext(), primary_key_names[0], ttl, std::move(rocksdb_dir), read_only);
}
std::shared_ptr<rocksdb::Statistics> StorageEmbeddedRocksDB::getRocksDBStatistics() const

View File

@ -33,7 +33,9 @@ public:
bool attach,
ContextPtr context_,
const String & primary_key_,
Int32 ttl_ = 0);
Int32 ttl_ = 0,
String rocksdb_dir_ = "",
bool read_only_ = false);
std::string getName() const override { return "EmbeddedRocksDB"; }
@ -82,6 +84,7 @@ private:
mutable std::shared_mutex rocksdb_ptr_mx;
String rocksdb_dir;
Int32 ttl;
bool read_only;
void initDB();
};

View File

@ -42,6 +42,18 @@ def test_valid_options(start_cluster):
DROP TABLE test;
"""
)
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only') PRIMARY KEY(key);
DROP TABLE test;
"""
)
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(10, '/var/lib/clickhouse/store/test_rocksdb_read_only', 1) PRIMARY KEY(key);
DROP TABLE test;
"""
)
def test_invalid_options(start_cluster):

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<clickhouse>
<rocksdb>
<options>
<max_background_jobs>8</max_background_jobs>
</options>
<column_family_options>
<num_levels>2</num_levels>
</column_family_options>
<tables>
<table>
<name>test</name>
<options>
<max_open_files>10000</max_open_files>
</options>
<column_family_options>
<max_bytes_for_level_base>14</max_bytes_for_level_base>
</column_family_options>
</table>
</tables>
</rocksdb>
</clickhouse>

View File

@ -0,0 +1,137 @@
# pylint: disable=unused-argument
# pylint: disable=redefined-outer-name
# pylint: disable=line-too-long
import pytest
from helpers.client import QueryRuntimeException
from helpers.cluster import ClickHouseCluster
cluster = ClickHouseCluster(__file__)
node = cluster.add_instance(
"node", main_configs=["configs/rocksdb.xml"], stay_alive=True
)
@pytest.fixture(scope="module")
def start_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def test_read_only(start_cluster):
# fail if read_only = true and directory does not exist.
with pytest.raises(QueryRuntimeException):
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only', 1) PRIMARY KEY(key);
"""
)
# create directory if read_only = false
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only') PRIMARY KEY(key);
INSERT INTO test (key, value) VALUES (0, 'a'), (1, 'b'), (2, 'c');
"""
)
# fail if create multiple non-read-only tables on the same directory
with pytest.raises(QueryRuntimeException):
node.query(
"""
CREATE TABLE test_fail (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only') PRIMARY KEY(key);
"""
)
with pytest.raises(QueryRuntimeException):
node.query(
"""
CREATE TABLE test_fail (key UInt64, value String) Engine=EmbeddedRocksDB(10, '/var/lib/clickhouse/store/test_rocksdb_read_only') PRIMARY KEY(key);
"""
)
# success if create multiple read-only tables on the same directory
node.query(
"""
CREATE TABLE test_1 (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only', 1) PRIMARY KEY(key);
DROP TABLE test_1;
"""
)
node.query(
"""
CREATE TABLE test_2 (key UInt64, value String) Engine=EmbeddedRocksDB(10, '/var/lib/clickhouse/store/test_rocksdb_read_only', 1) PRIMARY KEY(key);
DROP TABLE test_2;
"""
)
# success if create table on existing directory with no other tables on it
node.query(
"""
DROP TABLE test;
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(10, '/var/lib/clickhouse/store/test_rocksdb_read_only', 1) PRIMARY KEY(key);
"""
)
result = node.query("""SELECT count() FROM test;""")
assert result.strip() == "3"
# fail if insert into table with read_only = true
with pytest.raises(QueryRuntimeException):
node.query(
"""INSERT INTO test (key, value) VALUES (4, 'd');
"""
)
node.query(
"""
DROP TABLE test;
"""
)
def test_dirctory_missing_after_stop(start_cluster):
# for read_only = false
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only_missing') PRIMARY KEY(key);
"""
)
node.stop_clickhouse()
node.exec_in_container(
[
"bash",
"-c",
"rm -r /var/lib/clickhouse/store/test_rocksdb_read_only_missing",
]
)
node.start_clickhouse()
result = node.query(
"""INSERT INTO test (key, value) VALUES (0, 'a');
SELECT * FROM test;
"""
)
assert result.strip() == "0\ta"
node.query(
"""DROP TABLE test;
"""
)
# for read_only = true
node.query(
"""
CREATE TABLE test (key UInt64, value String) Engine=EmbeddedRocksDB(0, '/var/lib/clickhouse/store/test_rocksdb_read_only_missing', 1) PRIMARY KEY(key);
"""
)
node.stop_clickhouse()
node.exec_in_container(
[
"bash",
"-c",
"rm -r /var/lib/clickhouse/store/test_rocksdb_read_only_missing",
]
)
node.start_clickhouse()
with pytest.raises(QueryRuntimeException):
node.query("""INSERT INTO test (key, value) VALUES (1, 'b');""")
result = node.query("""SELECT * FROM test;""")
assert result.strip() == ""
node.query(
"""DROP TABLE test;
"""
)