folly/folly/coro/test/TimeoutTest.cpp

/*
 * 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.
 */

#include <folly/Portability.h>

#include <folly/experimental/coro/AsyncGenerator.h>
#include <folly/experimental/coro/BlockingWait.h>
#include <folly/experimental/coro/Collect.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/experimental/coro/Timeout.h>
#include <folly/futures/Future.h>
#include <folly/io/async/Request.h>
#include <folly/portability/GTest.h>

#include <chrono>
#include <stdexcept>

#if FOLLY_HAS_COROUTINES

using namespace std::chrono_literals;
using namespace folly;

struct Timeout {
  template <typename... Arg>
  auto operator()(Arg&&... args) {
    return coro::timeout(std::forward<Arg>(args)...);
  }
  using ExType = FutureTimeout;
};

struct TimeoutNoDiscard {
  template <typename... Arg>
  auto operator()(Arg&&... args) {
    return coro::timeoutNoDiscard(std::forward<Arg>(args)...);
  }
  using ExType = OperationCancelled;
};

template <typename T>
struct TimeoutFixture : public ::testing::Test {
  T fn;
};

using TimeoutTestTypes = ::testing::Types<Timeout, TimeoutNoDiscard>;
TYPED_TEST_SUITE(TimeoutFixture, TimeoutTestTypes);

TYPED_TEST(TimeoutFixture, CompletesSynchronously) {
  coro::blockingWait([&fn = this->fn]() -> coro::Task<> {
    // Completing synchronously with void
    co_await fn([]() -> coro::Task<void> { co_return; }(), 1s);

    // Completing synchronously with a value.

    auto result = co_await fn([]() -> coro::Task<int> { co_return 42; }(), 1s);
    EXPECT_EQ(42, result);

    // Test that it handles failing synchronously
    auto tryResult = co_await coro::co_awaitTry(fn(
        [&]() -> coro::Task<int> {
          if (true) {
            throw std::runtime_error{"bad value"};
          }
          co_return result;
        }(),
        1s));
    EXPECT_TRUE(tryResult.template hasException<std::runtime_error>());
  }());
}

TYPED_TEST(TimeoutFixture, CompletesWithinTimeout) {
  coro::blockingWait([&fn = this->fn]() -> coro::Task<> {
    // Completing synchronously with void
    co_await fn(
        []() -> coro::Task<void> {
          co_await coro::sleep(1ms);
          co_return;
        }(),
        1s);

    // Completing synchronously with a value.
    auto result = co_await fn(
        []() -> coro::Task<int> {
          co_await coro::sleep(1ms);
          co_return 42;
        }(),
        1s);
    EXPECT_EQ(42, result);

    // Test that it handles failing synchronously
    auto tryResult = co_await coro::co_awaitTry(fn(
        [&]() -> coro::Task<int> {
          co_await coro::sleep(1ms);
          if (true) {
            throw std::runtime_error{"bad value"};
          }
          co_return result;
        }(),
        1s));
    EXPECT_TRUE(tryResult.template hasException<std::runtime_error>());
  }());
}

TEST(TimeoutNoDiscard, ResultOnTimeout) {
  coro::blockingWait([]() -> coro::Task<> {
    co_await coro::timeoutNoDiscard(
        []() -> coro::Task<void> {
          co_await coro::sleepReturnEarlyOnCancel(10s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          co_return;
        }(),
        1ms);

    auto result = co_await coro::timeoutNoDiscard(
        []() -> coro::Task<int> {
          co_await coro::sleepReturnEarlyOnCancel(10s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          co_return 42;
        }(),
        1ms);
    EXPECT_EQ(42, result);

    struct sentinel : public std::exception {};
    auto tryResult = co_await coro::co_awaitTry(coro::timeoutNoDiscard(
        [&]() -> coro::Task<int> {
          co_await coro::sleepReturnEarlyOnCancel(10s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          throw sentinel{};
        }(),
        1ms));
    EXPECT_TRUE(tryResult.template hasException<sentinel>());
  }());
}

TYPED_TEST(TimeoutFixture, TimeoutElapsed) {
  using ExType = typename decltype(this->fn)::ExType;
  coro::blockingWait([&fn = this->fn]() -> coro::Task<> {
    // Completing synchronously with void
    auto start = std::chrono::steady_clock::now();
    folly::Try<void> voidResult = co_await coro::co_awaitTry(fn(
        []() -> coro::Task<void> {
          co_await coro::sleep(1s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          co_return;
        }(),
        5ms));
    auto elapsed = std::chrono::steady_clock::now() - start;
    EXPECT_LT(elapsed, 100ms);
    EXPECT_TRUE(voidResult.hasException<ExType>());

    // Completing synchronously with a value.
    start = std::chrono::steady_clock::now();
    auto result = co_await coro::co_awaitTry(fn(
        []() -> coro::Task<int> {
          co_await coro::sleep(1s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          co_return 42;
        }(),
        5ms));
    elapsed = std::chrono::steady_clock::now() - start;
    EXPECT_LT(elapsed, 100ms);
    EXPECT_TRUE(result.template hasException<ExType>());

    // Test that it handles failing synchronously
    start = std::chrono::steady_clock::now();
    auto failResult = co_await coro::co_awaitTry(fn(
        [&]() -> coro::Task<int> {
          co_await coro::sleep(1s);
          EXPECT_TRUE((co_await coro::co_current_cancellation_token)
                          .isCancellationRequested());
          if (true) {
            throw std::runtime_error{"bad value"};
          }
          co_return result;
        }(),
        5ms));
    elapsed = std::chrono::steady_clock::now() - start;
    EXPECT_LT(elapsed, 100ms);
    EXPECT_TRUE(result.template hasException<ExType>());
  }());
}

TYPED_TEST(TimeoutFixture, CancelParent) {
  coro::blockingWait([&fn = this->fn]() -> coro::Task<> {
    CancellationSource cancelSource;

    auto start = std::chrono::steady_clock::now();

    auto [cancelled, _] = co_await coro::collectAll(
        coro::co_withCancellation(
            cancelSource.getToken(),
            fn(
                []() -> coro::Task<bool> {
                  auto result = co_await coro::co_awaitTry(coro::sleep(5s));
                  co_return result.template hasException<OperationCancelled>();
                }(),
                10s)),
        [&]() -> coro::Task<void> {
          cancelSource.requestCancellation();
          co_return;
        }());

    auto elapsed = std::chrono::steady_clock::now() - start;
    EXPECT_LT(elapsed, 1s);

    EXPECT_TRUE(cancelled);
  }());
}

TYPED_TEST(TimeoutFixture, AsyncGenerator) {
  coro::blockingWait([&fn = this->fn]() -> coro::Task<> {
    // Completing synchronously with a value.
    auto result = co_await fn(
        []() -> coro::AsyncGenerator<int> { co_yield 42; }().next(), 1s);
    EXPECT_EQ(42, *result);

    // Test that it handles failing synchronously
    auto tryResult = co_await coro::co_awaitTry(fn(
        []() -> coro::AsyncGenerator<int> {
          co_yield coro::co_error(std::runtime_error{"bad value"});
        }()
                    .next(),
        1s));
    EXPECT_TRUE(tryResult.template hasException<std::runtime_error>());

    // Generator completing normally.
    result = co_await fn(
        []() -> coro::AsyncGenerator<int> { co_return; }().next(), 1s);
    EXPECT_FALSE(result);
  }());
}

TYPED_TEST(TimeoutFixture, RequestContextInCancellationCallback) {
  RequestContextScopeGuard guard;
  auto* ctx = folly::RequestContext::try_get();
  ASSERT_TRUE(ctx);

  bool cancelled = false;
  coro::blockingWait([&, &fn = this->fn]() -> coro::Task<> {
    co_await coro::co_awaitTry(fn(
        [&]() -> coro::Task<void> {
          CancellationCallback cb{
              co_await coro::co_current_cancellation_token, [&] {
                EXPECT_EQ(folly::RequestContext::try_get(), ctx);
                cancelled = true;
              }};
          co_await coro::sleep(1s);
        }(),
        5ms));
  }());
  ASSERT_TRUE(cancelled);
}

#endif // FOLLY_HAS_COROUTINES