@@ -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+
37283922struct StaticDestructionTester {
37293923 bool activated = false ;
37303924 ~StaticDestructionTester () {
0 commit comments