Skip to content

Commit f84351d

Browse files
archang19meta-codesync[bot]
authored andcommitted
Fix AbortIO hang when aborting multiple io_uring handles (facebook#14252)
Summary: Pull Request resolved: facebook#14252 Fixed a bug in PosixFileSystem::AbortIO that could cause an infinite hang when aborting multiple concurrent async IO handles. The bug occurred in the completion processing loop: when an io_uring completion arrived for a handle other than the one currently being waited for (io_handles[i]), the code would increment that handle's req_count but only mark it as finished if it also matched io_handles[i]. This meant completions for other handles were consumed but those handles were never marked as finished. Later, when iterating to those handles, the code would enter io_uring_wait_cqe expecting more completions, but they had already been consumed - causing an infinite hang. The fix aligns AbortIO's completion handling with what Poll() already does: mark handles as finished whenever their completions arrive, regardless of which handle we're currently waiting for in the outer loop. Only the break statement remains conditional on matching io_handles[i]. Reviewed By: anand1976 Differential Revision: D91070044 fbshipit-source-id: 47faf5f0df3e26a2aa83444bbac623f43f560933
1 parent f312633 commit f84351d

2 files changed

Lines changed: 203 additions & 3 deletions

File tree

env/env_test.cc

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3725,6 +3725,200 @@ TEST_F(TestAsyncRead, InterleavingIOUringOperations) {
37253725
#endif
37263726
}
37273727

3728+
// Helper function to run AbortIO test with parameterized read requests.
3729+
// Each request is specified as {offset, length}.
3730+
// use_direct_io: if true, opens the file with O_DIRECT to bypass page cache.
3731+
// iterations: number of times to repeat the test (useful for race conditions).
3732+
void TestAbortIOWithRequests(
3733+
Env* env, size_t file_size,
3734+
const std::vector<std::pair<uint64_t, size_t>>& read_specs,
3735+
bool use_direct_io = false, int iterations = 1) {
3736+
#if defined(ROCKSDB_IOURING_PRESENT)
3737+
fprintf(stderr,
3738+
"TestAbortIOWithRequests: file_size=%zu, num_reads=%zu, "
3739+
"direct_io=%d, iterations=%d\n",
3740+
file_size, read_specs.size(), use_direct_io, iterations);
3741+
std::shared_ptr<FileSystem> fs = env->GetFileSystem();
3742+
std::string fname = test::PerThreadDBPath(env, "testfile_abortio");
3743+
3744+
constexpr size_t kSectorSize = 4096;
3745+
3746+
for (int iter = 0; iter < iterations; iter++) {
3747+
// 1. Create test file of specified size using direct IO
3748+
{
3749+
std::unique_ptr<FSWritableFile> wfile;
3750+
FileOptions file_opts;
3751+
file_opts.use_direct_writes = true;
3752+
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
3753+
3754+
// Round up to full sectors for direct IO writes
3755+
size_t num_sectors = (file_size + kSectorSize - 1) / kSectorSize;
3756+
for (size_t i = 0; i < num_sectors; ++i) {
3757+
auto data = NewAligned(kSectorSize, static_cast<char>(i + 1));
3758+
Slice slice(data.get(), kSectorSize);
3759+
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
3760+
}
3761+
3762+
// Truncate to exact file size if not aligned to sector boundary
3763+
if (file_size % kSectorSize != 0) {
3764+
ASSERT_OK(wfile->Truncate(file_size, IOOptions(), nullptr));
3765+
}
3766+
3767+
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
3768+
}
3769+
3770+
// 2. Submit ReadAsync requests and immediately abort
3771+
{
3772+
FileOptions file_opts;
3773+
file_opts.use_direct_reads = use_direct_io;
3774+
std::unique_ptr<FSRandomAccessFile> file;
3775+
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
3776+
3777+
const size_t num_reads = read_specs.size();
3778+
IOOptions opts;
3779+
std::vector<void*> io_handles(num_reads);
3780+
std::vector<FSReadRequest> reqs(num_reads);
3781+
std::vector<std::unique_ptr<char, Deleter>> data;
3782+
std::vector<size_t> vals;
3783+
IOHandleDeleter del_fn;
3784+
3785+
// Initialize read requests from specs
3786+
for (size_t i = 0; i < num_reads; i++) {
3787+
reqs[i].offset = read_specs[i].first;
3788+
reqs[i].len = read_specs[i].second;
3789+
data.emplace_back(NewAligned(reqs[i].len, 0));
3790+
reqs[i].scratch = data.back().get();
3791+
vals.push_back(i);
3792+
}
3793+
3794+
// Callback
3795+
std::function<void(FSReadRequest&, void*)> callback =
3796+
[&](FSReadRequest& req, void* cb_arg) {
3797+
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
3798+
reqs[i].status = req.status;
3799+
};
3800+
3801+
// Submit all ReadAsync requests
3802+
for (size_t i = 0; i < num_reads; i++) {
3803+
void* cb_arg = static_cast<void*>(&(vals[i]));
3804+
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
3805+
&(io_handles[i]), &del_fn, nullptr);
3806+
if (s.IsNotSupported()) {
3807+
// io_uring not supported, clean up and skip
3808+
fprintf(stderr,
3809+
"WARNING: io_uring not supported, skipping test: %s\n",
3810+
s.ToString().c_str());
3811+
for (size_t j = 0; j < i; j++) {
3812+
if (io_handles[j]) {
3813+
del_fn(io_handles[j]);
3814+
}
3815+
}
3816+
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
3817+
return;
3818+
}
3819+
ASSERT_OK(s);
3820+
}
3821+
3822+
// Immediately call AbortIO - this should NOT hang
3823+
ASSERT_OK(fs->AbortIO(io_handles));
3824+
3825+
// Clean up handles
3826+
for (size_t i = 0; i < num_reads; i++) {
3827+
if (io_handles[i]) {
3828+
del_fn(io_handles[i]);
3829+
}
3830+
}
3831+
}
3832+
3833+
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
3834+
}
3835+
3836+
fprintf(stderr, "TestAbortIOWithRequests: completed %d iterations\n",
3837+
iterations);
3838+
#else
3839+
fprintf(stderr,
3840+
"TestAbortIOWithRequests: SKIPPED (ROCKSDB_IOURING_PRESENT not "
3841+
"defined)\n");
3842+
(void)env;
3843+
(void)file_size;
3844+
(void)read_specs;
3845+
(void)use_direct_io;
3846+
(void)iterations;
3847+
#endif
3848+
}
3849+
3850+
// Test overlapping reads at aligned offsets (multiples of 4KB)
3851+
TEST_F(TestAsyncRead, AbortIOOverlappingAligned) {
3852+
// 4 reads of 16KB each, overlapping by 8KB, all at 4KB-aligned offsets
3853+
// Read 0: [0, 16KB), Read 1: [8KB, 24KB), Read 2: [16KB, 32KB), Read 3:
3854+
// [24KB, 40KB)
3855+
std::vector<std::pair<uint64_t, size_t>> specs = {
3856+
{0, 16384},
3857+
{8192, 16384},
3858+
{16384, 16384},
3859+
{24576, 16384},
3860+
};
3861+
TestAbortIOWithRequests(env_, 64 * 1024, specs);
3862+
}
3863+
3864+
// Test reads at unaligned offsets (not multiples of 4KB)
3865+
TEST_F(TestAsyncRead, AbortIOUnalignedOffsets) {
3866+
// Reads starting at non-4KB-aligned offsets
3867+
std::vector<std::pair<uint64_t, size_t>> specs = {
3868+
{1000, 8192}, // starts at 1000 (unaligned)
3869+
{5000, 12288}, // starts at 5000 (unaligned), spans multiple sectors
3870+
{15000, 8192}, // starts at 15000 (unaligned)
3871+
{25500, 16384}, // starts at 25500 (unaligned)
3872+
};
3873+
TestAbortIOWithRequests(env_, 64 * 1024, specs);
3874+
}
3875+
3876+
// Test mix of aligned and unaligned, various sizes
3877+
TEST_F(TestAsyncRead, AbortIOMixedOffsets) {
3878+
std::vector<std::pair<uint64_t, size_t>> specs = {
3879+
{0, 4096}, // aligned, 1 sector
3880+
{1500, 8192}, // unaligned, 2 sectors
3881+
{4096, 20480}, // aligned, 5 sectors
3882+
{7000, 4096}, // unaligned, spans 2 sectors
3883+
{16384, 32768}, // aligned, 8 sectors
3884+
{50000, 8192}, // unaligned
3885+
};
3886+
TestAbortIOWithRequests(env_, 128 * 1024, specs);
3887+
}
3888+
3889+
// Stress test with many concurrent handles
3890+
TEST_F(TestAsyncRead, AbortIOStress) {
3891+
std::vector<std::pair<uint64_t, size_t>> specs;
3892+
// 16 overlapping reads with mixed alignment
3893+
for (int i = 0; i < 16; i++) {
3894+
uint64_t offset = i * 4000; // Not aligned to 4KB
3895+
size_t len = 8192 + (i % 4) * 4096; // 8KB to 20KB
3896+
specs.emplace_back(offset, len);
3897+
}
3898+
TestAbortIOWithRequests(env_, 256 * 1024, specs);
3899+
}
3900+
3901+
// Regression test for a fixed bug in AbortIO where out-of-order io_uring
3902+
// completions could cause an infinite hang. The bug occurred when completions
3903+
// for a different handle arrived while waiting for the current handle - the
3904+
// code would consume those completions but not mark the handle as finished,
3905+
// causing a hang when later iterating to that handle.
3906+
//
3907+
// Uses a large read (1MB) followed by a small read (4KB) with Direct I/O to
3908+
// maximize the chance of out-of-order completions. Runs 100 iterations to
3909+
// increase the likelihood of triggering the race condition.
3910+
TEST_F(TestAsyncRead, AbortIOReversedHandles) {
3911+
// Request 0: LARGE (1MB) at offset 0
3912+
// Request 1: SMALL (4KB) at offset 1MB
3913+
std::vector<std::pair<uint64_t, size_t>> specs = {
3914+
{0, 1024 * 1024}, // 1MB read
3915+
{1024 * 1024, 4096}, // 4KB read at 1MB offset
3916+
};
3917+
// 2MB file, Direct I/O enabled, 100 iterations
3918+
TestAbortIOWithRequests(env_, 2 * 1024 * 1024, specs,
3919+
/*use_direct_io=*/true, /*iterations=*/100);
3920+
}
3921+
37283922
struct StaticDestructionTester {
37293923
bool activated = false;
37303924
~StaticDestructionTester() {

env/fs_posix.cc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,14 +1249,20 @@ class PosixFileSystem : public FileSystem {
12491249
//
12501250
// Every handle has to wait for 2 requests completion: original one and
12511251
// the cancel request which is tracked by PosixHandle::req_count.
1252-
if (posix_handle->req_count == 2 &&
1253-
static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
1252+
// Note: We must mark is_finished and invoke the callback for ANY handle
1253+
// that reaches req_count == 2, not just the one we're currently waiting
1254+
// for (io_handles[i]). Otherwise, if completions arrive out of order,
1255+
// we consume another handle's completions without marking it finished,
1256+
// causing an infinite hang when we later wait for that handle.
1257+
if (posix_handle->req_count == 2) {
12541258
posix_handle->is_finished = true;
12551259
FSReadRequest req;
12561260
req.status = IOStatus::Aborted();
12571261
posix_handle->cb(req, posix_handle->cb_arg);
12581262

1259-
break;
1263+
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
1264+
break;
1265+
}
12601266
}
12611267
}
12621268
}

0 commit comments

Comments
 (0)