diff --git a/demos/file_demo.cc b/demos/file_demo.cc index 1c0f843775..14430b2dd7 100644 --- a/demos/file_demo.cc +++ b/demos/file_demo.cc @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -102,9 +103,72 @@ future<> demo_with_file() { }); } +future<> demo_with_file_close_on_failure() { + fmt::print("\nDemonstrating with_file_close_on_failure():\n"); + return tmp_dir::do_with_thread([] (tmp_dir& t) { + auto rnd = std::mt19937(std::random_device()()); + auto dist = std::uniform_int_distribution(0, std::numeric_limits::max()); + auto wbuf = temporary_buffer::aligned(aligned_size, aligned_size); + sstring meta_filename = (t.get_path() / "meta_file").native(); + sstring data_filename = (t.get_path() / "data_file").native(); + + // with_file_close_on_failure will close the opened file only if + // `make_file_output_stream` returns an error. Otherwise, in the error-free path, + // the opened file is moved to `file_output_stream` that in-turn closes it + // when the stream is closed. + auto make_output_stream = [] (const sstring filename) { + return with_file_close_on_failure(open_file_dma(std::move(filename), open_flags::rw | open_flags::create), [] (file f) { + return make_file_output_stream(std::move(f), aligned_size); + }); + }; + + // writes the buffer one byte at a time, to demonstrate output stream + auto write_to_stream = [] (output_stream& o, const temporary_buffer& wbuf) { + return seastar::do_for_each(wbuf, [&o] (char c) { + return o.write(&c, 1); + }).finally([&o] { + return o.close(); + }); + }; + + // print the data_filename into the write buffer + std::fill(wbuf.get_write(), wbuf.get_write() + aligned_size, 0); + std::copy(data_filename.cbegin(), data_filename.cend(), wbuf.get_write()); + + // and write it to `meta_filename` + fmt::print(" writing \"{}\" into {}\n", data_filename, meta_filename); + + // with_file_close_on_failure will close the opened file only if + // `make_file_output_stream` returns an error. Otherwise, in the error-free path, + // the opened file is moved to `file_output_stream` that in-turn closes it + // when the stream is closed. + output_stream o = make_output_stream(meta_filename).get0(); + + write_to_stream(o, wbuf).get(); + + // now write some random data into data_filename + fmt::print(" writing random data into {}\n", data_filename); + std::generate(wbuf.get_write(), wbuf.get_write() + aligned_size, [&dist, &rnd] { return dist(rnd); }); + + o = make_output_stream(data_filename).get0(); + + write_to_stream(o, wbuf).get(); + + // verify the data via meta_filename + fmt::print(" verifying data...\n"); + auto rbuf = temporary_buffer::aligned(aligned_size, aligned_size); + + with_file(open_data_file(meta_filename, rbuf), [&rbuf, &wbuf] (file& f) { + return verify_data_file(f, rbuf, wbuf); + }).get(); + }); +} + int main(int ac, char** av) { app_template app; return app.run(ac, av, [] { - return demo_with_file(); + return demo_with_file().then([] { + return demo_with_file_close_on_failure(); + }); }); } diff --git a/include/seastar/core/file.hh b/include/seastar/core/file.hh index a048774a09..8c140781ff 100644 --- a/include/seastar/core/file.hh +++ b/include/seastar/core/file.hh @@ -446,8 +446,42 @@ auto with_file(future file_fut, Func func) noexcept { }); } +/// \brief Helper for ensuring a file is closed if \c func fails. +/// +/// The file provided by the \c file_fut future is passed to \c func. +/// * If func throws an exception E, the file is closed and we return +/// a failed future with E. +/// * If func returns a value V, the file is not closed and we return +/// a future with V. +/// Note that when an exception is not thrown, it is the +/// responsibility of func to make sure the file will be closed. It +/// can close the file itself, return it, or store it somewhere. +/// +/// \param file_fut A future that produces a file +/// \param func A function that uses a file +/// \returns the future returned by \c func, or an exceptional future if \c file_fut failed or a nested exception if closing the file failed. +template +SEASTAR_CONCEPT( requires std::invocable && std::is_nothrow_move_constructible_v ) +auto with_file_close_on_failure(future file_fut, Func func) noexcept { + static_assert(std::is_nothrow_move_constructible_v, "Func's move constructor must not throw"); + return file_fut.then([func = std::move(func)] (file f) mutable { + return do_with(std::move(f), [func = std::move(func)] (file& f) mutable { + return futurize_invoke(std::move(func), f).then_wrapped([&f] (auto ret) mutable { + if (!ret.failed()) { + return ret; + } + return ret.finally([&f] { + // If f.close() fails, return that as nested exception. + return f.close(); + }); + }); + }); + }); +} + /// \example file_demo.cc /// A program demonstrating the use of \ref seastar::with_file +/// and \ref seastar::with_file_close_on_failure /// \brief A shard-transportable handle to a file /// diff --git a/tests/unit/file_io_test.cc b/tests/unit/file_io_test.cc index a087faf8d8..a26bfb420d 100644 --- a/tests/unit/file_io_test.cc +++ b/tests/unit/file_io_test.cc @@ -592,3 +592,31 @@ SEASTAR_TEST_CASE(test_open_error_with_file) { BOOST_REQUIRE(!got_exception); }); } + +SEASTAR_TEST_CASE(test_with_file_close_on_failure) { + return tmp_dir::do_with_thread([] (tmp_dir& t) { + auto oflags = open_flags::rw | open_flags::create | open_flags::truncate; + sstring filename = (t.get_path() / "testfile.tmp").native(); + + auto orig_umask = umask(0); + + // error-free case + auto ref = with_file_close_on_failure(open_file_dma(filename, oflags), [] (file& f) { + return f; + }).get0(); + auto st = ref.stat().get0(); + ref.close().get(); + BOOST_CHECK_EQUAL(st.st_mode & static_cast(file_permissions::all_permissions), static_cast(file_permissions::default_file_permissions)); + + // close-on-error case + BOOST_REQUIRE_THROW(with_file_close_on_failure(open_file_dma(filename, oflags), [&ref] (file& f) { + ref = f; + throw std::runtime_error("expected exception"); + }).get(), std::runtime_error); + + // verify that file was auto-closed on error + BOOST_REQUIRE_THROW(ref.stat().get(), std::system_error); + + umask(orig_umask); + }); +}