folly/folly/futures/Cleanup.h

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <memory>
#include <mutex>

#include <glog/logging.h>

#include <folly/futures/Future.h>

namespace folly {

// Structured Async Cleanup
//

// Structured Async Cleanup - traits
//

namespace detail {
struct cleanup_fn {
  template <
      class T,
      class R = decltype(std::declval<T>().cleanup()),
      std::enable_if_t<std::is_same_v<R, folly::SemiFuture<folly::Unit>>, int> =
          0>
  R operator()(T&& t) const {
    return ((T&&)t).cleanup();
  }
};
} // namespace detail

template <class T>
constexpr bool is_cleanup_v = folly::is_invocable_v<detail::cleanup_fn, T>;

template <typename T>
using is_cleanup = std::bool_constant<is_cleanup_v<T>>;

// Structured Async Cleanup
//
// This helps compose a task with async cleanup
// The task result is stored until cleanup completes and is then produced
// The cleanup task is not allowed to fail.
//
// This can be used with collectAll to combine multiple async resources
//
// ensureCleanupAfterTask(collectAll(a.run(), b.run()), collectAll(a.cleanup(),
// b.cleanup())).wait();
//
template <typename T>
folly::SemiFuture<T> ensureCleanupAfterTask(
    folly::SemiFuture<T> task, folly::SemiFuture<folly::Unit> cleanup) {
  return folly::makeSemiFuture()
      .deferValue([task_ = std::move(task)](folly::Unit) mutable {
        return std::move(task_);
      })
      .defer([cleanup_ = std::move(cleanup)](folly::Try<T> taskResult) mutable {
        return std::move(cleanup_).defer(
            [taskResult_ = std::move(taskResult)](folly::Try<folly::Unit> t) {
              if (t.hasException()) {
                terminate_with<std::logic_error>("cleanup must not throw");
              }
              return std::move(taskResult_).value();
            });
      });
}

// Structured Async Cleanup
//
// This implementation is a base class that collects a set of cleanup tasks
// and runs them in reverse order.
//
// A class derived from Cleanup
//  - only allows cleanup to be run once
//  - is required to complete cleanup before running the destructor
//  - *should not* run cleanup tasks. Running the cleanup task should be
//    delegated to the owner of the derived class
//  - *should not* be owned by a shared_ptr. Cleanup is intended to remove
//    shared ownership.
//
class Cleanup {
 public:
  Cleanup() : safe_to_destruct_(false), cleanup_(folly::makeSemiFuture()) {}
  ~Cleanup() {
    if (!safe_to_destruct_) {
      LOG(FATAL) << "Cleanup must complete before it is destructed.";
    }
  }

  // Returns: a SemiFuture that, just like destructors, sequences the cleanup
  // tasks added in reverse of the order they were added.
  //
  // calls to cleanup() do not mutate state. The returned SemiFuture, once
  // it has been given an executor, does mutate state and must not overlap with
  // any calls to addCleanup().
  //
  folly::SemiFuture<folly::Unit> cleanup() {
    return folly::makeSemiFuture()
        .deferValue([this](folly::Unit) {
          if (!cleanup_.valid()) {
            LOG(FATAL) << "cleanup already run - cleanup task invalid.";
          }
          return std::move(cleanup_);
        })
        .defer([this](folly::Try<folly::Unit> t) {
          if (t.hasException()) {
            LOG(FATAL) << "Cleanup actions must be noexcept.";
          }
          this->safe_to_destruct_ = true;
        });
  }

 protected:
  // includes the provided SemiFuture under the scope of this.
  //
  // when the cleanup() for this started it will get this SemiFuture first.
  //
  // order matters, just like destructors, cleanup tasks will be run in reverse
  // of the order they were added.
  //
  // all gets will use the Executor provided to the SemiFuture returned by
  // cleanup()
  //
  // calls to addCleanup() must not overlap with each other and must not overlap
  // with a running SemiFuture returned from addCleanup().
  //
  void addCleanup(folly::SemiFuture<folly::Unit> c) {
    if (!cleanup_.valid()) {
      LOG(FATAL)
          << "Cleanup::addCleanup must not be called after Cleanup::cleanup.";
    }
    cleanup_ = std::move(c).deferValue(
        [nested = std::move(cleanup_)](folly::Unit) mutable {
          return std::move(nested);
        });
  }

  // includes the provided model of Cleanup under the scope of this
  //
  // when the cleanup() for this started it will cleanup this first.
  //
  // order matters, just like destructors, cleanup tasks will be run in reverse
  // of the order they were added.
  //
  // all gets will use the Executor provided to the SemiFuture returned by
  // cleanup()
  //
  // calls to addCleanup() must not overlap with each other and must not overlap
  // with a running SemiFuture returned from addCleanup().
  //
  template <
      class OtherCleanup,
      std::enable_if_t<is_cleanup_v<OtherCleanup>, int> = 0>
  void addCleanup(OtherCleanup&& c) {
    addCleanup(((OtherCleanup&&)c).cleanup());
  }

 private:
  bool safe_to_destruct_;
  folly::SemiFuture<folly::Unit> cleanup_;
};

} // namespace folly