1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/util/coro_utils.h
Josh Kang 88752298ff Add native coroutine read dispatch to RocksDB (#15073)
Summary:
## Summary

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

The callback-based async read APIs make coroutine-native implementations and StackableDB composition awkward because wrappers must bridge callbacks back into tasks. This change introduces `CoroDB` as a side capability exposing lazy `GetCoroutine` and `MultiGetCoroutine` tasks. `DBImpl` and `CompactedDBImpl` implement this capability directly, while `DB::GetAsync` and `DB::MultiGetAsync` remain available as compatibility entry points with synchronous fallback.

The native interface should also be more efficient because callers directly await the DB coroutine. It avoids the callback/Baton adapter, callback context handoff, and explicit rescheduling required for each request. Native tasks must run on the DB filesystem read executor's EventBase; debug builds verify this affinity.

Public API documentation, a release note, and native-coroutine support in `db_bench` and `db_stress` are included.

## Dispatch

  DB::GetAsync
    -> GetCoroDB() on the concrete DB
    -> virtual CoroDB::GetCoroutine(...)
       -> DBImpl::GetCoroutine or CompactedDBImpl::GetCoroutine
    -> AsyncCallback::OnComplete(...)

Virtual dispatch selects the coroutine implementation belonging to the concrete DB while preserving the existing callback API.

## Statistics

Coroutine reads preserve RocksDB's TLS statistics behavior while keeping concurrent requests isolated. Each top-level task copies only the caller's statistics configuration—`PerfLevel`, per-level perf context enablement, and IO statistics enablement—into fresh request-local `PerfContext` and `IOStatsContext` objects. Existing counters are not inherited. If both perf and IO statistics are disabled, no request statistics context is installed.

Folly `RequestContext` preserves these contexts across suspension and resumption on the read executor's EventBase. At completion, RocksDB publishes the request's collected values to TLS, allowing the awaiting caller to use `get_perf_context()` and `get_iostats_context()` as after a synchronous read. Callers must consume or copy them before leaving that EventBase. Statistics scopes are installed only by top-level coroutine reads and cannot be nested.

## Benchmarks

Compared the previous `GetAsync` callback bridge with native `GetCoroutine`. Both variants used optimized builds, the same compacted DB, a fixed seed, eight executor threads, ten jobs per thread, and 2.56M reads per invocation. Results are the mean of three independent invocations using the default RocksDB block cache. A sequential read warmed the OS page cache before the non-direct-read measurements.

| Workload | `GetAsync` bridge | Native `GetCoroutine` | Change |
| Direct I/O | 237,710 ops/s | 243,588 ops/s | +2.47% |
| Warm OS page cache | 783,859 ops/s | 891,531 ops/s | +13.74% |

The direct-I/O difference is within observed run-to-run variation. When reads are served from the OS page cache and API overhead is a larger portion of latency, the native interface improves throughput by 13.74%. Every invocation completed all requested reads.

```bash
db_bench \
  --benchmarks=readrandomcoroutine \
  --db=$CORO_BENCH_DB \
  --use_existing_db=true \
  --num=2000000 \
  --value_size=1024 \
  --compression_type=none \
  --threads=8 \
  --coro_jobs_per_thread=10 \
  --reads=320000 \
  --use_direct_reads=<true|false> \
  --perf_level=1 \
  --statistics=true \
  --seed=20260807
```

Reviewed By: xingbowang

Differential Revision: D115232511

fbshipit-source-id: 30b48da2a798094d9080178f5430aae26e6c5954
2026-08-10 09:43:05 -07:00

135 lines
5.6 KiB
C++

// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// 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).
#if defined(USE_COROUTINES)
#include "folly/coro/Coroutine.h"
#include "folly/coro/Nothrow.h"
#include "folly/coro/Task.h"
#endif
#include "rocksdb/rocksdb_namespace.h"
// This file has two sections. The first section applies to all instances of
// header file inclusion and has an include guard. The second section is
// meant for multiple inclusions in the same source file, and is idempotent.
namespace ROCKSDB_NAMESPACE {
#ifndef UTIL_CORO_UTILS_H_
#define UTIL_CORO_UTILS_H_
#if defined(USE_COROUTINES)
// The following macros expand to regular and coroutine function
// declarations for a given function
#define DECLARE_SYNC_AND_ASYNC(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__); \
folly::coro::Task<__ret_type__> __func_name__##Coroutine(__VA_ARGS__);
#define DECLARE_SYNC_AND_ASYNC_VIRTUAL(__ret_type__, __func_name__, ...) \
virtual __ret_type__ __func_name__(__VA_ARGS__); \
virtual folly::coro::Task<__ret_type__> __func_name__##Coroutine(__VA_ARGS__);
#define DECLARE_SYNC_AND_ASYNC_OVERRIDE(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__) override; \
folly::coro::Task<__ret_type__> __func_name__##Coroutine(__VA_ARGS__) \
override;
#define DECLARE_SYNC_AND_ASYNC_CONST(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__) const; \
folly::coro::Task<__ret_type__> __func_name__##Coroutine(__VA_ARGS__) const;
#define DECLARE_SYNC_AND_ASYNC_TEMPLATE_CONST(__template_decl__, __ret_type__, \
__func_name__, ...) \
__template_decl__ __ret_type__ __func_name__(__VA_ARGS__) const; \
__template_decl__ folly::coro::Task<__ret_type__> __func_name__##Coroutine( \
__VA_ARGS__) const;
constexpr bool using_coroutines() { return true; }
#else // !USE_COROUTINES
// The following macros expand to a regular function declaration for a given
// function
#define DECLARE_SYNC_AND_ASYNC(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__);
#define DECLARE_SYNC_AND_ASYNC_VIRTUAL(__ret_type__, __func_name__, ...) \
virtual __ret_type__ __func_name__(__VA_ARGS__);
#define DECLARE_SYNC_AND_ASYNC_OVERRIDE(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__) override;
#define DECLARE_SYNC_AND_ASYNC_CONST(__ret_type__, __func_name__, ...) \
__ret_type__ __func_name__(__VA_ARGS__) const;
#define DECLARE_SYNC_AND_ASYNC_TEMPLATE_CONST(__template_decl__, __ret_type__, \
__func_name__, ...) \
__template_decl__ __ret_type__ __func_name__(__VA_ARGS__) const;
constexpr bool using_coroutines() { return false; }
#endif // USE_COROUTINES
#endif // UTIL_CORO_UTILS_H_
// The following section of the file is meant to be included twice in a
// source file - once defining WITH_COROUTINES and once defining
// WITHOUT_COROUTINES
#undef DEFINE_SYNC_AND_ASYNC
#undef CO_AWAIT
#undef CO_RETURN
#if defined(WITH_COROUTINES) && defined(USE_COROUTINES)
// This macro should be used in the beginning of the function
// definition. The declaration should have been done using one of the
// DECLARE_SYNC_AND_ASYNC* macros. It expands to the return type and
// the function name with the Coroutine suffix. For example -
// DEFINE_SYNC_AND_ASYNC(int, foo)(bool bar) {}
// would expand to -
// folly::coro::Task<int> fooCoroutine(bool bar) {}
#define DEFINE_SYNC_AND_ASYNC(__ret_type__, __func_name__) \
folly::coro::Task<__ret_type__> __func_name__##Coroutine
// This macro should be used to call a function that might be a
// coroutine. It expands to the correct function name and prefixes
// the co_await operator if necessary. For example -
// s = CO_AWAIT(foo, true);
// if the code is compiled WITH_COROUTINES, would expand to
// s = co_await folly::coro::co_nothrow(fooCoroutine(true));
// if compiled WITHOUT_COROUTINES, would expand to
// s = foo(true);
// co_nothrow drops folly's per-await exception-rethrow scaffolding (RocksDB
// uses Status, not exceptions). The call args are passed into CO_AWAIT (not
// appended after it) so co_nothrow can wrap the whole call.
#define CO_AWAIT(__func_name__, ...) \
co_await folly::coro::co_nothrow(__func_name__##Coroutine(__VA_ARGS__))
#define CO_RETURN co_return
#elif defined(WITHOUT_COROUTINES)
// This macro should be used in the beginning of the function
// definition. The declaration should have been done using one of the
// DECLARE_SYNC_AND_ASYNC* macros. It expands to the return type and
// the function name without the Coroutine suffix. For example -
// DEFINE_SYNC_AND_ASYNC(int, foo)(bool bar) {}
// would expand to -
// int foo(bool bar) {}
#define DEFINE_SYNC_AND_ASYNC(__ret_type__, __func_name__) \
__ret_type__ __func_name__
// This macro should be used to call a function that might be a
// coroutine. It expands to the correct function name and prefixes
// the co_await operator if necessary. For example -
// s = CO_AWAIT(foo, true);
// if the code is compiled WITH_COROUTINES, would expand to
// s = co_await folly::coro::co_nothrow(fooCoroutine(true));
// if compiled WITHOUT_COROUTINES, would expand to
// s = foo(true);
#define CO_AWAIT(__func_name__, ...) __func_name__(__VA_ARGS__)
#define CO_RETURN return
#endif // DO_NOT_USE_COROUTINES
} // namespace ROCKSDB_NAMESPACE