Fix IN_MEMORY_ANALYTICAL storage GC (#1025)
This commit is contained in:
parent
b25e9968ee
commit
5b1ba10183
@ -585,6 +585,12 @@ Result<std::optional<VertexAccessor>> Storage::Accessor::DeleteVertex(VertexAcce
|
|||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
||||||
vertex_ptr->deleted = true;
|
vertex_ptr->deleted = true;
|
||||||
|
|
||||||
|
// Need to inform the next CollectGarbage call that there are some
|
||||||
|
// non-transactional deletions that need to be collected
|
||||||
|
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
||||||
|
storage_->gc_full_scan_vertices_delete_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
return std::make_optional<VertexAccessor>(vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_,
|
return std::make_optional<VertexAccessor>(vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_,
|
||||||
config_, true);
|
config_, true);
|
||||||
}
|
}
|
||||||
@ -655,6 +661,12 @@ Result<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>> Stor
|
|||||||
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
CreateAndLinkDelta(&transaction_, vertex_ptr, Delta::RecreateObjectTag());
|
||||||
vertex_ptr->deleted = true;
|
vertex_ptr->deleted = true;
|
||||||
|
|
||||||
|
// Need to inform the next CollectGarbage call that there are some
|
||||||
|
// non-transactional deletions that need to be collected
|
||||||
|
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
||||||
|
storage_->gc_full_scan_vertices_delete_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
return std::make_optional<ReturnType>(
|
return std::make_optional<ReturnType>(
|
||||||
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_, true},
|
VertexAccessor{vertex_ptr, &transaction_, &storage_->indices_, &storage_->constraints_, config_, true},
|
||||||
std::move(deleted_edges));
|
std::move(deleted_edges));
|
||||||
@ -865,6 +877,12 @@ Result<std::optional<EdgeAccessor>> Storage::Accessor::DeleteEdge(EdgeAccessor *
|
|||||||
auto *edge_ptr = edge_ref.ptr;
|
auto *edge_ptr = edge_ref.ptr;
|
||||||
CreateAndLinkDelta(&transaction_, edge_ptr, Delta::RecreateObjectTag());
|
CreateAndLinkDelta(&transaction_, edge_ptr, Delta::RecreateObjectTag());
|
||||||
edge_ptr->deleted = true;
|
edge_ptr->deleted = true;
|
||||||
|
|
||||||
|
// Need to inform the next CollectGarbage call that there are some
|
||||||
|
// non-transactional deletions that need to be collected
|
||||||
|
if (transaction_.storage_mode == StorageMode::IN_MEMORY_ANALYTICAL) {
|
||||||
|
storage_->gc_full_scan_edges_delete_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateAndLinkDelta(&transaction_, from_vertex, Delta::AddOutEdgeTag(), edge_type, to_vertex, edge_ref);
|
CreateAndLinkDelta(&transaction_, from_vertex, Delta::AddOutEdgeTag(), edge_type, to_vertex, edge_ref);
|
||||||
@ -1466,26 +1484,42 @@ Transaction Storage::CreateTransaction(IsolationLevel isolation_level, StorageMo
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <bool force>
|
template <bool force>
|
||||||
void Storage::CollectGarbage() {
|
void Storage::CollectGarbage(std::unique_lock<utils::RWLock> main_guard) {
|
||||||
if constexpr (force) {
|
// NOTE: You do not need to consider cleanup of deleted object that occurred in
|
||||||
// We take the unique lock on the main storage lock so we can forcefully clean
|
// different storage modes within the same CollectGarbage call. This is because
|
||||||
// everything we can
|
// SetStorageMode will ensure CollectGarbage is called before any new transactions
|
||||||
if (!main_lock_.try_lock()) {
|
// with the new storage mode can start.
|
||||||
CollectGarbage<false>();
|
|
||||||
return;
|
// SetStorageMode will pass its unique_lock of main_lock_. We will use that lock,
|
||||||
|
// as reacquiring the lock would cause deadlock. Otherwise, we need to get our own
|
||||||
|
// lock.
|
||||||
|
if (!main_guard.owns_lock()) {
|
||||||
|
if constexpr (force) {
|
||||||
|
// We take the unique lock on the main storage lock, so we can forcefully clean
|
||||||
|
// everything we can
|
||||||
|
if (!main_lock_.try_lock()) {
|
||||||
|
CollectGarbage<false>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Because the garbage collector iterates through the indices and constraints
|
||||||
|
// to clean them up, it must take the main lock for reading to make sure that
|
||||||
|
// the indices and constraints aren't concurrently being modified.
|
||||||
|
main_lock_.lock_shared();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Because the garbage collector iterates through the indices and constraints
|
MG_ASSERT(main_guard.mutex() == std::addressof(main_lock_), "main_guard should be only for the main_lock_");
|
||||||
// to clean them up, it must take the main lock for reading to make sure that
|
|
||||||
// the indices and constraints aren't concurrently being modified.
|
|
||||||
main_lock_.lock_shared();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
utils::OnScopeExit lock_releaser{[&] {
|
utils::OnScopeExit lock_releaser{[&] {
|
||||||
if constexpr (force) {
|
if (!main_guard.owns_lock()) {
|
||||||
main_lock_.unlock();
|
if constexpr (force) {
|
||||||
|
main_lock_.unlock();
|
||||||
|
} else {
|
||||||
|
main_lock_.unlock_shared();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
main_lock_.unlock_shared();
|
main_guard.unlock();
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@ -1515,14 +1549,18 @@ void Storage::CollectGarbage() {
|
|||||||
deleted_vertices_->swap(current_deleted_vertices);
|
deleted_vertices_->swap(current_deleted_vertices);
|
||||||
deleted_edges_->swap(current_deleted_edges);
|
deleted_edges_->swap(current_deleted_edges);
|
||||||
|
|
||||||
|
auto const need_full_scan_vertices = gc_full_scan_vertices_delete_.exchange(false);
|
||||||
|
auto const need_full_scan_edges = gc_full_scan_edges_delete_.exchange(false);
|
||||||
|
|
||||||
// Flag that will be used to determine whether the Index GC should be run. It
|
// Flag that will be used to determine whether the Index GC should be run. It
|
||||||
// should be run when there were any items that were cleaned up (there were
|
// should be run when there were any items that were cleaned up (there were
|
||||||
// updates between this run of the GC and the previous run of the GC). This
|
// updates between this run of the GC and the previous run of the GC). This
|
||||||
// eliminates high CPU usage when the GC doesn't have to clean up anything.
|
// eliminates high CPU usage when the GC doesn't have to clean up anything.
|
||||||
bool run_index_cleanup = !committed_transactions_->empty() || !garbage_undo_buffers_->empty();
|
bool run_index_cleanup = !committed_transactions_->empty() || !garbage_undo_buffers_->empty() ||
|
||||||
|
need_full_scan_vertices || need_full_scan_edges;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// We don't want to hold the lock on commited transactions for too long,
|
// We don't want to hold the lock on committed transactions for too long,
|
||||||
// because that prevents other transactions from committing.
|
// because that prevents other transactions from committing.
|
||||||
Transaction *transaction;
|
Transaction *transaction;
|
||||||
{
|
{
|
||||||
@ -1718,11 +1756,37 @@ void Storage::CollectGarbage() {
|
|||||||
MG_ASSERT(edge_acc.remove(edge), "Invalid database state!");
|
MG_ASSERT(edge_acc.remove(edge), "Invalid database state!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXPENSIVE full scan, is only run if an IN_MEMORY_ANALYTICAL transaction involved any deletions
|
||||||
|
// TODO: implement a fast internal iteration inside the skip_list (to avoid unnecessary find_node calls),
|
||||||
|
// accessor.remove_if([](auto const & item){ return item.delta == nullptr && item.deleted;});
|
||||||
|
// alternatively, an auxiliary data structure within skip_list to track these, hence a full scan wouldn't be needed
|
||||||
|
// we will wait for evidence that this is needed before doing so.
|
||||||
|
if (need_full_scan_vertices) {
|
||||||
|
auto vertex_acc = vertices_.access();
|
||||||
|
for (auto &vertex : vertex_acc) {
|
||||||
|
// a deleted vertex which as no deltas must have come from IN_MEMORY_ANALYTICAL deletion
|
||||||
|
if (vertex.delta == nullptr && vertex.deleted) {
|
||||||
|
vertex_acc.remove(vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPENSIVE full scan, is only run if an IN_MEMORY_ANALYTICAL transaction involved any deletions
|
||||||
|
if (need_full_scan_edges) {
|
||||||
|
auto edge_acc = edges_.access();
|
||||||
|
for (auto &edge : edge_acc) {
|
||||||
|
// a deleted edge which as no deltas must have come from IN_MEMORY_ANALYTICAL deletion
|
||||||
|
if (edge.delta == nullptr && edge.deleted) {
|
||||||
|
edge_acc.remove(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell the linker he can find the CollectGarbage definitions here
|
// tell the linker it can find the CollectGarbage definitions here
|
||||||
template void Storage::CollectGarbage<true>();
|
template void Storage::CollectGarbage<true>(std::unique_lock<utils::RWLock>);
|
||||||
template void Storage::CollectGarbage<false>();
|
template void Storage::CollectGarbage<false>(std::unique_lock<utils::RWLock>);
|
||||||
|
|
||||||
bool Storage::InitializeWalFile() {
|
bool Storage::InitializeWalFile() {
|
||||||
if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::PERIODIC_SNAPSHOT_WITH_WAL)
|
if (config_.durability.snapshot_wal_mode != Config::Durability::SnapshotWalMode::PERIODIC_SNAPSHOT_WITH_WAL)
|
||||||
@ -2042,8 +2106,8 @@ utils::FileRetainer::FileLockerAccessor::ret_type Storage::UnlockPath() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Storage::FreeMemory() {
|
void Storage::FreeMemory(std::unique_lock<utils::RWLock> main_guard) {
|
||||||
CollectGarbage<true>();
|
CollectGarbage<true>(std::move(main_guard));
|
||||||
|
|
||||||
// SkipList is already threadsafe
|
// SkipList is already threadsafe
|
||||||
vertices_.run_gc();
|
vertices_.run_gc();
|
||||||
@ -2299,7 +2363,11 @@ IsolationLevel Storage::GetIsolationLevel() const noexcept { return isolation_le
|
|||||||
|
|
||||||
void Storage::SetStorageMode(StorageMode storage_mode) {
|
void Storage::SetStorageMode(StorageMode storage_mode) {
|
||||||
std::unique_lock main_guard{main_lock_};
|
std::unique_lock main_guard{main_lock_};
|
||||||
storage_mode_ = storage_mode;
|
// Only if we change storage_mode do we want to force storage cleanup
|
||||||
|
if (storage_mode_ != storage_mode) {
|
||||||
|
storage_mode_ = storage_mode;
|
||||||
|
FreeMemory(std::move(main_guard));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageMode Storage::GetStorageMode() { return storage_mode_; }
|
StorageMode Storage::GetStorageMode() { return storage_mode_; }
|
||||||
|
@ -509,7 +509,7 @@ class Storage final {
|
|||||||
|
|
||||||
std::vector<ReplicaInfo> ReplicasInfo();
|
std::vector<ReplicaInfo> ReplicasInfo();
|
||||||
|
|
||||||
void FreeMemory();
|
void FreeMemory(std::unique_lock<utils::RWLock> main_guard = {});
|
||||||
|
|
||||||
enum class SetIsolationLevelError : uint8_t { DisabledForAnalyticalMode };
|
enum class SetIsolationLevelError : uint8_t { DisabledForAnalyticalMode };
|
||||||
|
|
||||||
@ -543,7 +543,7 @@ class Storage final {
|
|||||||
/// @throw std::system_error
|
/// @throw std::system_error
|
||||||
/// @throw std::bad_alloc
|
/// @throw std::bad_alloc
|
||||||
template <bool force>
|
template <bool force>
|
||||||
void CollectGarbage();
|
void CollectGarbage(std::unique_lock<utils::RWLock> main_guard = {});
|
||||||
|
|
||||||
bool InitializeWalFile();
|
bool InitializeWalFile();
|
||||||
void FinalizeWalFile();
|
void FinalizeWalFile();
|
||||||
@ -618,6 +618,10 @@ class Storage final {
|
|||||||
// storage.
|
// storage.
|
||||||
utils::Synchronized<std::list<Gid>, utils::SpinLock> deleted_edges_;
|
utils::Synchronized<std::list<Gid>, utils::SpinLock> deleted_edges_;
|
||||||
|
|
||||||
|
// Flags to inform CollectGarbage that it needs to do the more expensive full scans
|
||||||
|
std::atomic<bool> gc_full_scan_vertices_delete_ = false;
|
||||||
|
std::atomic<bool> gc_full_scan_edges_delete_ = false;
|
||||||
|
|
||||||
// Durability
|
// Durability
|
||||||
std::filesystem::path snapshot_directory_;
|
std::filesystem::path snapshot_directory_;
|
||||||
std::filesystem::path wal_directory_;
|
std::filesystem::path wal_directory_;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2022 Memgraph Ltd.
|
// Copyright 2023 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// Use of this software is governed by the Business Source License
|
||||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -391,7 +391,7 @@ class SkipListGc final {
|
|||||||
///
|
///
|
||||||
/// The implementation is based on the work described in the paper
|
/// The implementation is based on the work described in the paper
|
||||||
/// "A Provably Correct Scalable Concurrent Skip List"
|
/// "A Provably Correct Scalable Concurrent Skip List"
|
||||||
/// https://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/OPODIS2006-BA.pdf
|
/// http://people.csail.mit.edu/shanir/publications/OPODIS2006-BA.pdf
|
||||||
///
|
///
|
||||||
/// The proposed implementation is in Java so the authors don't worry about
|
/// The proposed implementation is in Java so the authors don't worry about
|
||||||
/// garbage collection. This implementation uses the garbage collector that is
|
/// garbage collection. This implementation uses the garbage collector that is
|
||||||
@ -640,6 +640,7 @@ class SkipList final {
|
|||||||
skiplist_ = other.skiplist_;
|
skiplist_ = other.skiplist_;
|
||||||
id_ = other.id_;
|
id_ = other.id_;
|
||||||
other.skiplist_ = nullptr;
|
other.skiplist_ = nullptr;
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions that return an Iterator (or ConstIterator) to the beginning of
|
/// Functions that return an Iterator (or ConstIterator) to the beginning of
|
||||||
@ -775,6 +776,7 @@ class SkipList final {
|
|||||||
skiplist_ = other.skiplist_;
|
skiplist_ = other.skiplist_;
|
||||||
id_ = other.id_;
|
id_ = other.id_;
|
||||||
other.skiplist_ = nullptr;
|
other.skiplist_ = nullptr;
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConstIterator begin() const { return ConstIterator{skiplist_->head_->nexts[0].load(std::memory_order_acquire)}; }
|
ConstIterator begin() const { return ConstIterator{skiplist_->head_->nexts[0].load(std::memory_order_acquire)}; }
|
||||||
|
@ -57,6 +57,7 @@ add_subdirectory(transaction_queue)
|
|||||||
add_subdirectory(mock_api)
|
add_subdirectory(mock_api)
|
||||||
add_subdirectory(load_csv)
|
add_subdirectory(load_csv)
|
||||||
add_subdirectory(init_file_flags)
|
add_subdirectory(init_file_flags)
|
||||||
|
add_subdirectory(analytical_mode)
|
||||||
|
|
||||||
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
|
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
|
||||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
6
tests/e2e/analytical_mode/CMakeLists.txt
Normal file
6
tests/e2e/analytical_mode/CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function(copy_analytical_mode_e2e_python_files FILE_NAME)
|
||||||
|
copy_e2e_python_files(analytical_mode ${FILE_NAME})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
copy_analytical_mode_e2e_python_files(common.py)
|
||||||
|
copy_analytical_mode_e2e_python_files(free_memory.py)
|
30
tests/e2e/analytical_mode/common.py
Normal file
30
tests/e2e/analytical_mode/common.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright 2023 Memgraph Ltd.
|
||||||
|
#
|
||||||
|
# Use of this software is governed by the Business Source License
|
||||||
|
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
|
# License, and you may not use this file except in compliance with the Business Source License.
|
||||||
|
#
|
||||||
|
# As of the Change Date specified in that file, in accordance with
|
||||||
|
# the Business Source License, use of this software will be governed
|
||||||
|
# by the Apache License, Version 2.0, included in the file
|
||||||
|
# licenses/APL.txt.
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import mgclient
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]:
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def connect(**kwargs) -> mgclient.Connection:
|
||||||
|
connection = mgclient.connect(host="localhost", port=7687, **kwargs)
|
||||||
|
connection.autocommit = True
|
||||||
|
yield connection
|
||||||
|
cursor = connection.cursor()
|
||||||
|
execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")
|
||||||
|
execute_and_fetch_all(cursor, "STORAGE MODE IN_MEMORY_TRANSACTIONAL;")
|
63
tests/e2e/analytical_mode/free_memory.py
Normal file
63
tests/e2e/analytical_mode/free_memory.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Copyright 2023 Memgraph Ltd.
|
||||||
|
#
|
||||||
|
# Use of this software is governed by the Business Source License
|
||||||
|
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
|
# License, and you may not use this file except in compliance with the Business Source License.
|
||||||
|
#
|
||||||
|
# As of the Change Date specified in that file, in accordance with
|
||||||
|
# the Business Source License, use of this software will be governed
|
||||||
|
# by the Apache License, Version 2.0, included in the file
|
||||||
|
# licenses/APL.txt.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from common import connect, execute_and_fetch_all
|
||||||
|
|
||||||
|
|
||||||
|
def check_storage_info(cursor, expected_values):
|
||||||
|
cursor.execute("SHOW STORAGE INFO")
|
||||||
|
config = cursor.fetchall()
|
||||||
|
|
||||||
|
for conf in config:
|
||||||
|
conf_name = conf[0]
|
||||||
|
if conf_name in expected_values:
|
||||||
|
assert expected_values[conf_name] == conf[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_analytical_mode_objects_are_actually_deleted_when_asked(connect):
|
||||||
|
"""Tests objects are actually freed when deleted in analytical mode."""
|
||||||
|
|
||||||
|
expected_values = {
|
||||||
|
"vertex_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = connect.cursor()
|
||||||
|
check_storage_info(cursor, expected_values)
|
||||||
|
|
||||||
|
cursor.execute("STORAGE MODE IN_MEMORY_ANALYTICAL;")
|
||||||
|
cursor.execute("MERGE (n) DELETE n;")
|
||||||
|
cursor.execute("FREE MEMORY;")
|
||||||
|
|
||||||
|
check_storage_info(cursor, expected_values)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analytical_mode_objects_are_actually_deleted_when_storage_mode_changes(connect):
|
||||||
|
"""Tests objects are actually freed when deleted in analytical mode."""
|
||||||
|
|
||||||
|
expected_values = {
|
||||||
|
"vertex_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = connect.cursor()
|
||||||
|
check_storage_info(cursor, expected_values)
|
||||||
|
|
||||||
|
cursor.execute("STORAGE MODE IN_MEMORY_ANALYTICAL;")
|
||||||
|
cursor.execute("MERGE (n) DELETE n;")
|
||||||
|
cursor.execute("STORAGE MODE IN_MEMORY_TRANSACTIONAL;")
|
||||||
|
|
||||||
|
check_storage_info(cursor, expected_values)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(pytest.main([__file__, "-rA"]))
|
14
tests/e2e/analytical_mode/workloads.yaml
Normal file
14
tests/e2e/analytical_mode/workloads.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
analytical_mode_cluster: &analytical_mode_cluster
|
||||||
|
cluster:
|
||||||
|
main:
|
||||||
|
args: ["--bolt-port", "7687", "--log-level=TRACE"]
|
||||||
|
log_file: "analytical_mode.log"
|
||||||
|
setup_queries: []
|
||||||
|
validation_queries: []
|
||||||
|
|
||||||
|
|
||||||
|
workloads:
|
||||||
|
- name: "Analytical mode checks"
|
||||||
|
binary: "tests/e2e/pytest_runner.sh"
|
||||||
|
args: ["analytical_mode/free_memory.py"]
|
||||||
|
<<: *analytical_mode_cluster
|
Loading…
Reference in New Issue
Block a user