blob: b81ce31a5c1bffb84fd2e114ad03ccd62cad199a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// Most code of this file is copied and modified from rocksdb SyncPoint.
// And modified by Gavin (github.com/gavinchou).
#pragma once
// clang-format off
#include <functional>
#include <mutex>
#include <string>
#include <vector>
namespace doris::cloud {
// This class provides facility to reproduce race conditions deterministically
// in unit tests.
// Developer could specify sync points in the codebase via TEST_SYNC_POINT.
// Each sync point represents a position in the execution stream of a thread.
// In the unit test, 'Happens After' relationship among sync points could be
// setup via SyncPoint::load_dependency, to reproduce a desired interleave of
// threads execution.
// Refer to (DBTest,TransactionLogIteratorRace), for an example use case.
class SyncPoint {
public:
static SyncPoint* get_instance();
SyncPoint(const SyncPoint&) = delete;
SyncPoint& operator=(const SyncPoint&) = delete;
~SyncPoint();
struct SyncPointPair {
std::string predecessor;
std::string successor;
};
// call once at the beginning of a test to setup the dependency between
// sync points
//
// Example:
// load_dependency({{"point1", "point2"},
// {"point2", "point3"},
// {"point3", "point4"}});
//
// test case thread thread for object being tested
// | |
// | |
// | \-------------0-------------\ |
// | \-> x sync point1 set in code
// | /----------1----------------/ |
// point2 o <-/ /-> x sync point4 set in code
// | / |
// z / |
// z /---------2-----------/ | there may be nothing
// | / | between point1 point4
// point3 o --/ | they are for sync
// | | between test case and object
// v v
//
// vertical arrow means the procedure of each thread, the running order will
// be:
// test case thread -> point1 -> point2 -> point3 -> point4 -> object being
// tested
//
// we may do a lot of things between point2 and point3, say, change the
// object's status, call another method, propagate data race and etc.
void load_dependency(const std::vector<SyncPointPair>& dependencies);
// call once at the beginning of a test to setup the dependency between
// sync points and setup markers indicating the successor is only enabled
// when it is processed on the same thread as the predecessor.
// When adding a marker, it implicitly adds a dependency for the marker pair.
void load_dependency_and_markers(
const std::vector<SyncPointPair>& dependencies,
const std::vector<SyncPointPair>& markers);
// The argument to the callback is passed through from
// TEST_SYNC_POINT_CALLBACK(); nullptr if TEST_SYNC_POINT or
// TEST_IDX_SYNC_POINT was used.
void set_call_back(const std::string& point,
const std::function<void(void*)>& callback);
// Clear callback function by point
void clear_call_back(const std::string& point);
// Clear all call back functions.
void clear_all_call_backs();
// Enable sync point processing (disabled on startup)
void enable_processing();
// Disable sync point processing
void disable_processing();
// Remove the execution trace of all sync points
void clear_trace();
// Triggered by TEST_SYNC_POINT, blocking execution until all predecessors
// are executed.
// And/or call registered callback function, with argument `cb_arg`
void process(const std::string& point, void* cb_arg = nullptr);
// TODO: it might be useful to provide a function that blocks until all
// sync points are cleared.
// We want this to be public so we can subclass the implementation
struct Data;
private:
// Singleton
SyncPoint();
Data* impl_; // implementation which is hidden in cpp file
};
} // namespace doris::cloud
// TEST_SYNC_POINT is no op in release build.
// Turn on this feature by defining the macro
#ifndef UNIT_TEST
# define TEST_SYNC_POINT(x)
# define TEST_IDX_SYNC_POINT(x, index)
# define TEST_SYNC_POINT_CALLBACK(x, y)
# define TEST_SYNC_POINT_RETURN_WITH_VALUE(sync_point_name, ret_val_ptr)
# define TEST_SYNC_POINT_RETURN_WITH_VOID(sync_point_name)
// seldom called
# define INIT_SYNC_POINT_SINGLETONS()
#else
// Use TEST_SYNC_POINT to specify sync points inside code base.
// Sync points can have happens-after depedency on other sync points,
// configured at runtime via SyncPoint::load_dependency. This could be
// utilized to re-produce race conditions between threads.
# define TEST_SYNC_POINT(x) doris::cloud::SyncPoint::get_instance()->process(x)
# define TEST_IDX_SYNC_POINT(x, index) \
doris::cloud::SyncPoint::get_instance()->process(x + std::to_string(index))
# define TEST_SYNC_POINT_CALLBACK(x, y) \
doris::cloud::SyncPoint::get_instance()->process(x, y)
# define INIT_SYNC_POINT_SINGLETONS() \
(void)doris::cloud::SyncPoint::get_instance();
/**
* Inject return points for testing.
*
* Currently we can only insert more points to get context from tested thread
* and process in testing thread, e.g.
*
* tested thread:
* ...
* TEST_SYNC_POINT_CALLBACK("point_ctx", ptr_to_ctx);
* TEST_SYNC_POINT_RETURN_WITH_VALUE("point_ret", ptr_to_ret_val);
* ...
*
* testing thread:
* sync_point->add("point_ctx", [&ctx](void* ptr_to_ctx) { ctx = ptr_to_ctx; });
* sync_point->add("point_ret", [](void* ptr_to_ret) {...});
* sync_point->add("point_ret::pred", [&ctx](void* pred) { pred = *ctx ? true : false; });
*
* See sync_point_test.cpp for more details.
*/
#pragma GCC diagnostic ignored "-Waddress"
# define TEST_SYNC_POINT_RETURN_WITH_VALUE(sync_point_name, ret_val_ptr) \
static_assert(ret_val_ptr != nullptr, "ret_val_ptr cannot be nullptr");\
TEST_SYNC_POINT_CALLBACK(sync_point_name, ret_val_ptr); \
{ \
bool pred = false; \
TEST_SYNC_POINT_CALLBACK(sync_point_name"::pred", &pred); \
if (pred) return *ret_val_ptr; \
}
# define TEST_SYNC_POINT_RETURN_WITH_VOID(sync_point_name) \
{ \
bool pred = false; \
TEST_SYNC_POINT_CALLBACK(sync_point_name"::pred", &pred); \
if (pred) return; \
}
#endif // UNIT_TEST
// TODO: define injection point in production env.
// the `if` expr can be live configure of the application
#define ENABLE_INJECTION_POINT 0
#ifndef ENABLE_INJECTION_POINT
# define TEST_INJECTION_POINT(x)
# define TEST_IDX_TEST_INJECTION_POINT(x, index)
# define TEST_INJECTION_POINT_CALLBACK(x, y)
# define INIT_INJECTION_POINT_SINGLETONS()
#else
# define TEST_INJECTION_POINT(x) \
if (ENABLE_INJECTION_POINT) { \
doris::cloud::SyncPoint::get_instance()->process(x); \
}
# define TEST_IDX_INJECTION_POINT(x, index) \
if (ENABLE_INJECTION_POINT) { \
doris::cloud::SyncPoint::get_instance()->process(x + std::to_string(index)); \
}
# define TEST_INJECTION_POINT_CALLBACK(x, y) \
if (ENABLE_INJECTION_POINT) { \
doris::cloud::SyncPoint::get_instance()->process(x, y); \
}
# define INIT_INJECTION_POINT_SINGLETONS() \
if (ENABLE_INJECTION_POINT) { \
(void)doris::cloud::SyncPoint::get_instance(); \
}
#endif // ENABLE_INJECTION_POINT
// clang-format on