Skip to content

Commit

Permalink
file: add with_file_close_on_error helper
Browse files Browse the repository at this point in the history
Auto-closes a file passed to the `func` parameter if
`func` returns an exception.  If close() fails too,
it's exception will be returned as a nested exception of
the outer one.

Signed-off-by: Benny Halevy <bhalevy@scylladb.com>
  • Loading branch information
bhalevy committed Jul 12, 2020
1 parent 4fca836 commit aff077a
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 1 deletion.
66 changes: 65 additions & 1 deletion demos/file_demo.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <seastar/core/aligned_buffer.hh>
#include <seastar/core/file.hh>
#include <seastar/core/fstream.hh>
#include <seastar/core/seastar.hh>
#include <seastar/core/sstring.hh>
#include <seastar/core/temporary_buffer.hh>
Expand Down Expand Up @@ -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<char>(0, std::numeric_limits<char>::max());
auto wbuf = temporary_buffer<char>::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<char>& o, const temporary_buffer<char>& 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<char> 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<char>::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();
});
});
}
34 changes: 34 additions & 0 deletions include/seastar/core/file.hh
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,42 @@ auto with_file(future<file> 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 <typename Func>
SEASTAR_CONCEPT( requires std::invocable<Func, file&> && std::is_nothrow_move_constructible_v<Func> )
auto with_file_close_on_failure(future<file> file_fut, Func func) noexcept {
static_assert(std::is_nothrow_move_constructible_v<Func>, "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
///
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/file_io_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<mode_t>(file_permissions::all_permissions), static_cast<mode_t>(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);
});
}

0 comments on commit aff077a

Please sign in to comment.