1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/test_util/sync_point.h
Peter Dillinger af0eae4de3 Rename TransactionLogIterator to WalIterator and document its behavior (#15056)
Summary:
`TransactionLogIterator` iterates the write-ahead log. It does not return transactions, does not require `TransactionDB`, and in fact returns `Status::NotSupported()` for two of the three `TransactionDB` write policies (WritePrepared and WriteUnprepared). The name predates `TransactionDB` and comes from "transaction log" being an old synonym for WAL. Everything else in this header was already renamed away from that terminology -- `LogFile` -> `WalFile`, `VectorLogPtr` -> `VectorWalPtr` -- and the C API has long used `rocksdb_wal_iterator_t` and `rocksdb_wal_iter_*`. This finishes the job.

The public class is now `WalIterator` in `include/rocksdb/wal_iterator.h`. `include/rocksdb/transaction_log.h` remains as a forwarding header and `TransactionLogIterator` remains as an alias, both marked deprecated by comment only (not `[[deprecated]]`) so existing users compiling with -Werror are unaffected, matching how `LogFile` and `VectorLogPtr` were retired. `DB::GetUpdatesSince()` keeps its name.

More importantly, the documentation was incomplete in ways that could cost a user data, so this states plainly what the API has always done:

- It only sees the WAL. Writes made with `WriteOptions::disableWAL`, and sequence numbers consumed by `IngestExternalFile()`, advance the DB sequence number without producing a WAL record. They leave permanent holes that end iteration. This was not documented anywhere, so it was unreasonable to expect a user to know those features silently break WAL tailing.
- The three iterator states, in particular that `!Valid()` with an OK `status()` means caught up rather than finished, and that calling `Next()` again is the intended way to tail a DB. The old comment on `Next()` said "REQUIRES: Valid() to be true", which forbids the primary use case.
- `Status::TryAgain`, which a caller must handle but which appeared nowhere in the public headers.
- That `GetUpdatesSince(seq)` silently starts later when `seq` is unavailable. Combined with the above this is the sharp edge: recovering from a spent iterator means calling `GetUpdatesSince()` again, so the recovery step is exactly where data can be skipped unnoticed. A consumer applying these batches to a copy of the DB diverges permanently and undetectably if it misses one, so the header now says such consumers must verify continuity themselves using `BatchResult::sequence` and `WriteBatch::Count()`.

Almost all of this diff is mechanical: renaming the type and its files, updating `#include` paths, rewriting comments, and updating build lists (`src.mk`, `CMakeLists.txt`, `BUCK`, `java/Makefile`, `java/CMakeLists.txt`). `db/c.cc` is regenerated from `tools/c_api_gen/c_base.cc`. Reviewers should focus on the four places that are not:

1. `WalIteratorImpl::UpdateCurrentWriteBatch()` in `db/wal_iterator_impl.cc` (renamed from `db/transaction_log_impl.cc`) drops dead code. On a sequence discontinuity it set `current_status_` to `NotFound` and then called `SeekToStartSequence()`, which returns immediately when `current_status_` is not OK. That guard and the early return in `Next()` were both added by fe63899d1 ("Add checks to GetUpdatesSince", https://github.com/facebook/rocksdb/issues/9459), which made the reseek unreachable without mentioning it. The surviving behavior -- a discontinuity ends the run with `NotFound` -- is what the class has always documented, so this deletes the unreachable reseek and the comment claiming it can restore an OK status rather than resurrecting it. There is no observable change: `Valid()` is `started_ && is_valid_`, `is_valid_` is false either way, and a non-OK `current_status_` already makes `Next()` a no-op.
2. `WalIteratorImpl::IsBatchExpected()` in the same file drops "Log iterator will reseek the correct batch." from its info log message, since it no longer does.
3. `org.rocksdb.WalIterator` now holds the implementation and the nested `BatchResult`, and `org.rocksdb.TransactionLogIterator` becomes a subclass of it. Existing Java code keeps compiling, `RocksDB.getUpdatesSince()` keeps its return type, and `TransactionLogIterator.BatchResult` still resolves through inheritance. Because the native methods are now declared on `WalIterator`, the JNI entry points in `java/rocksjni/wal_iterator.cc` (renamed from `transaction_log.cc`) become `Java_org_rocksdb_WalIterator_*`.
4. `BatchResultJni::getJClass()` in `java/rocksjni/portal.h` looks up `org/rocksdb/WalIterator$BatchResult` to match.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/15056

Test Plan:
Adds three tests to `db_log_iter_test.cc` that pin the semantics now being documented, none of which had coverage:

- `CaughtUpThenResumes` -- an iterator that has run out of records is `!Valid()` with an OK `status()`, and a later write to the same WAL is picked up by calling `Next()` again. This is the tailing pattern the old `Next()` comment prohibited.
- `GapFromDisableWAL` -- a `disableWAL` write between two normal writes ends the run with `NotFound` instead of skipping to the later sequence number, and the iterator stays spent across further `Next()` calls.
- `SilentlySkipsUnavailableStart` -- `GetUpdatesSince(1)` where sequence 1 never reached the WAL returns OK and positions at sequence 2, documenting the silent skip.

`IngestExternalFile()` is documented as the other source of holes but is not covered by a test; it needs `SstFileWriter` setup and is worth a follow-up.

The Java rename is verified by `WalIteratorTest`, which declares `WalIterator` while `RocksDB.getUpdatesSince()` still returns `TransactionLogIterator`, so the compatibility subclass is exercised on every use. It is otherwise an identifier-only rename of the previous test.

Renames the C++ test fixture to `DBWalIteratorTest`, drops the now-redundant `TransactionLogIterator` prefix from its test names, and updates the comments in `wal_manager.cc`, `sync_point.h` and `fs_posix.cc` that refer to those tests by name. Those comments already pointed at the wrong fixture and file (`DBTest` in `db_test.cc`) before this change.

Reviewed By: xingbowang

Differential Revision: D114809792

Pulled By: pdillinger

fbshipit-source-id: d3cfd23867b4738b62a096db1dbc895bada069b7
2026-08-05 13:21:37 -07:00

218 lines
8.2 KiB
C++

// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <assert.h>
#include <atomic>
#include <functional>
#include <string>
#include <vector>
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#ifdef NDEBUG
// empty in release build
#define TEST_KILL_RANDOM_WITH_WEIGHT(kill_point, rocksdb_kill_odds_weight)
#define TEST_KILL_RANDOM(kill_point)
#else
namespace ROCKSDB_NAMESPACE {
// To avoid crashing always at some frequently executed codepaths (during
// kill random test), use this factor to reduce odds
#define REDUCE_ODDS 2
#define REDUCE_ODDS2 4
// A class used to pass when a kill point is reached.
struct KillPoint {
public:
// This is only set from db_stress.cc and for testing only.
// If non-zero, kill at various points in source code with probability 1/this
int rocksdb_kill_odds = 0;
// If kill point has a prefix on this list, will skip killing.
std::vector<std::string> rocksdb_kill_exclude_prefixes;
// Kill the process with probability 1/odds for testing.
void TestKillRandom(std::string kill_point, int odds,
const std::string& srcfile, int srcline);
static KillPoint* GetInstance();
};
#define TEST_KILL_RANDOM_WITH_WEIGHT(kill_point, rocksdb_kill_odds_weight) \
{ \
KillPoint::GetInstance()->TestKillRandom( \
kill_point, rocksdb_kill_odds_weight, __FILE__, __LINE__); \
}
#define TEST_KILL_RANDOM(kill_point) TEST_KILL_RANDOM_WITH_WEIGHT(kill_point, 1)
} // namespace ROCKSDB_NAMESPACE
#endif
#ifdef NDEBUG
#define TEST_SYNC_POINT(x)
#define TEST_IDX_SYNC_POINT(x, index)
#define TEST_SYNC_POINT_CALLBACK(x, y)
#define INIT_SYNC_POINT_SINGLETONS()
#else
namespace ROCKSDB_NAMESPACE {
// 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::LoadDependency, to reproduce a desired interleave of
// threads execution.
// Refer to (DBWalIteratorTest,Race), for an example use
// case.
class SyncPoint {
public:
static SyncPoint* GetInstance();
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. Specifically, execution will not be allowed to proceed past
// each successor until execution has reached the corresponding predecessor,
// in any thread.
void LoadDependency(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 LoadDependencyAndMarkers(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 SetCallBack(const std::string& point,
const std::function<void(void*)>& callback);
// Clear callback function by point
void ClearCallBack(const std::string& point);
// Clear all call back functions.
void ClearAllCallBacks();
// enable sync point processing (disabled on startup)
void EnableProcessing();
// disable sync point processing
void DisableProcessing();
// remove the execution trace of all sync points
void ClearTrace();
// 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 Slice& point, void* cb_arg = nullptr);
// template gets length of const string at compile time,
// avoiding strlen() at runtime
template <size_t kLen>
void Process(const char (&point)[kLen], void* cb_arg = nullptr) {
static_assert(kLen > 0, "Must not be empty");
assert(point[kLen - 1] == '\0');
Process(Slice(point, kLen - 1), cb_arg);
}
// 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_;
};
// Sets up sync points to mock direct IO instead of actually issuing direct IO
// to the file system.
void SetupSyncPointsToMockDirectIO();
} // namespace ROCKSDB_NAMESPACE
// Use TEST_SYNC_POINT to specify sync points inside code base.
// Sync points can have happens-after dependency on other sync points,
// configured at runtime via SyncPoint::LoadDependency. This could be
// utilized to re-produce race conditions between threads.
// See DBWalIteratorTest.Race in db_log_iter_test.cc for an example use
// case.
// TEST_SYNC_POINT is no op in release build.
#define TEST_SYNC_POINT(x) \
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->Process(x)
#define TEST_IDX_SYNC_POINT(x, index) \
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->Process(x + \
std::to_string(index))
#define TEST_SYNC_POINT_CALLBACK(x, y) \
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->Process(x, y)
#define INIT_SYNC_POINT_SINGLETONS() \
(void)ROCKSDB_NAMESPACE::SyncPoint::GetInstance();
#endif // NDEBUG
// Callback sync point for any read IO errors that should be ignored by
// the fault injection framework
// Disable in release mode
#ifdef NDEBUG
#define IGNORE_STATUS_IF_ERROR(_status_)
#else
#define IGNORE_STATUS_IF_ERROR(_status_) \
{ \
if (!_status_.ok()) { \
TEST_SYNC_POINT("FaultInjectionIgnoreError"); \
} \
}
#endif // NDEBUG
// An alternative to assert() that is more test-friendly than using
// ASSERT_DEATH. Relies on exception propagation.
#ifdef NDEBUG
#define testable_assert(cond)
#else
namespace ROCKSDB_NAMESPACE {
// Intentionally not based on std::exception to reduce places where this
// would be caught
struct TestableAssertionFailure {};
// Tracks whether to throw on testable_assert failure instead of aborting.
// This is an atomic counter for re-entrancy / thread-safety.
extern std::atomic<int> g_throw_on_testable_assertion_failure;
} // namespace ROCKSDB_NAMESPACE
#define testable_assert(cond) \
do { \
if (ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.load( \
std::memory_order_relaxed) > 0) { \
if (cond) { \
} else \
throw ROCKSDB_NAMESPACE::TestableAssertionFailure(); \
} else { \
assert(cond); \
} \
} while (0) // require ; in caller
#define ASSERT_TESTABLE_FAILURE(expr) \
do { \
ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.fetch_add( \
1, std::memory_order_relaxed); \
ASSERT_THROW(expr, ROCKSDB_NAMESPACE::TestableAssertionFailure); \
ROCKSDB_NAMESPACE::g_throw_on_testable_assertion_failure.fetch_sub( \
1, std::memory_order_relaxed); \
} while (0) // require ; in caller
#endif