| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you 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 "iceberg/util/task_group.h" | |
| #include <atomic> | |
| #include <concepts> | |
| #include <functional> | |
| #include <memory> | |
| #include <utility> | |
| #include <vector> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/result.h" | |
| #include "iceberg/test/executor.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/test/retry.h" | |
| #include "iceberg/util/functional.h" | |
| namespace iceberg { | |
| namespace { | |
| RetryConfig FastRetryConfig(int32_t num_retries = 2) { | |
| return RetryConfig{.num_retries = num_retries, | |
| .min_wait_ms = 1, | |
| .max_wait_ms = 1, | |
| .total_timeout_ms = 0}; | |
| } | |
| } // namespace | |
| TEST(TaskGroupCompileTest, TaskConcepts) { | |
| auto move_only_mutable_lambda = [value = std::make_unique<int>(1)]() mutable -> Status { | |
| return *value == 1 ? Status{} : IOError("unexpected value"); | |
| }; | |
| using MoveOnlyMutableLambda = decltype(move_only_mutable_lambda); | |
| auto move_only_const_lambda = [value = std::make_unique<int>(1)]() -> Status { | |
| return *value == 1 ? Status{} : IOError("unexpected value"); | |
| }; | |
| using MoveOnlyConstLambda = decltype(move_only_const_lambda); | |
| auto copyable_mutable_lambda = [attempt = 0]() mutable -> Status { | |
| return ++attempt > 0 ? Status{} : Status{}; | |
| }; | |
| using CopyableMutableLambda = decltype(copyable_mutable_lambda); | |
| static_assert(!std::copy_constructible<FnOnce<void()>>); | |
| static_assert(!std::default_initializable<FnOnce<void()>>); | |
| static_assert(std::move_constructible<FnOnce<void()>>); | |
| static_assert(internal::OnceStatusTask<MoveOnlyMutableLambda>); | |
| static_assert(!internal::OnceStatusTask<MoveOnlyMutableLambda&>); | |
| static_assert(internal::OnceStatusTask<MoveOnlyConstLambda>); | |
| static_assert(!internal::OnceStatusTask<MoveOnlyConstLambda&>); | |
| static_assert(internal::OnceStatusTask<CopyableMutableLambda>); | |
| static_assert(internal::OnceStatusTask<CopyableMutableLambda&>); | |
| static_assert(!internal::RepeatableStatusTask<MoveOnlyMutableLambda>); | |
| static_assert(internal::RepeatableStatusTask<MoveOnlyConstLambda>); | |
| static_assert(internal::RepeatableStatusTask<CopyableMutableLambda>); | |
| static_assert(!internal::RetryableStatusTask<MoveOnlyMutableLambda>); | |
| static_assert(!internal::RetryableStatusTask<MoveOnlyMutableLambda&>); | |
| static_assert(internal::RetryableStatusTask<MoveOnlyConstLambda>); | |
| static_assert(!internal::RetryableStatusTask<MoveOnlyConstLambda&>); | |
| static_assert(internal::RetryableStatusTask<CopyableMutableLambda>); | |
| static_assert(internal::RetryableStatusTask<CopyableMutableLambda&>); | |
| } | |
| TEST(FnOnceTest, SupportsMoveOnlyCapture) { | |
| auto value = std::make_unique<int>(41); | |
| FnOnce<int()> task([value = std::move(value)]() { return *value + 1; }); | |
| EXPECT_EQ(std::move(task)(), 42); | |
| } | |
| TEST(TaskGroupTest, UsesExecutor) { | |
| test::ThreadExecutor executor; | |
| TaskGroup group; | |
| bool ran = false; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([&]() -> Status { | |
| ran = true; | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_TRUE(ran); | |
| EXPECT_EQ(executor.submit_count(), 1); | |
| } | |
| TEST(TaskGroupTest, ReturnsSubmitError) { | |
| test::ThreadExecutor executor(ServiceUnavailable("executor busy")); | |
| TaskGroup group; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([]() -> Status { return {}; }); | |
| EXPECT_THAT(std::move(group).Run(), IsError(ErrorKind::kServiceUnavailable)); | |
| EXPECT_EQ(executor.submit_count(), 1); | |
| } | |
| TEST(TaskGroupTest, DirectMoveOnlyTask) { | |
| TaskGroup group; | |
| auto value = std::make_unique<int>(7); | |
| int observed = 0; | |
| group.Submit([value = std::move(value), &observed]() mutable -> Status { | |
| observed = *value; | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(observed, 7); | |
| } | |
| TEST(TaskGroupTest, ClearsExecutor) { | |
| test::ThreadExecutor executor; | |
| TaskGroup group; | |
| int call_count = 0; | |
| group.SetExecutor(std::ref(executor)); | |
| group.SetExecutor(std::nullopt); | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(call_count, 1); | |
| EXPECT_EQ(executor.submit_count(), 0); | |
| } | |
| TEST(TaskGroupTest, FluentSubmit) { | |
| test::ThreadExecutor executor; | |
| std::atomic<int> call_count = 0; | |
| auto status = TaskGroup() | |
| .SetExecutor(std::ref(executor)) | |
| .Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }) | |
| .Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }) | |
| .Run(); | |
| EXPECT_THAT(status, IsOk()); | |
| EXPECT_EQ(call_count.load(std::memory_order_relaxed), 2); | |
| } | |
| TEST(TaskGroupTest, DirectAggregatesErrors) { | |
| TaskGroup group; | |
| int call_count = 0; | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| return IOError("first failure"); | |
| }); | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| return ValidationFailed("second failure"); | |
| }); | |
| auto status = std::move(group).Run(); | |
| EXPECT_THAT(status, IsError(ErrorKind::kIOError)); | |
| EXPECT_THAT(status, HasErrorMessage("Task group failed with 2 errors")); | |
| EXPECT_THAT(status, HasErrorMessage("first failure")); | |
| EXPECT_THAT(status, HasErrorMessage("second failure")); | |
| EXPECT_EQ(call_count, 2); | |
| } | |
| TEST(TaskGroupTest, ParallelSubmitsAll) { | |
| test::ThreadExecutor executor; | |
| TaskGroup group; | |
| std::atomic<int> call_count = 0; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(call_count.load(std::memory_order_relaxed), 2); | |
| EXPECT_EQ(executor.submit_count(), 2); | |
| } | |
| TEST(TaskGroupTest, ParallelAggregatesErrors) { | |
| test::ThreadExecutor executor; | |
| TaskGroup group; | |
| std::atomic<int> call_count = 0; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return IOError("first failure"); | |
| }); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return ValidationFailed("second failure"); | |
| }); | |
| auto status = std::move(group).Run(); | |
| EXPECT_THAT(status, IsError(ErrorKind::kIOError)); | |
| EXPECT_THAT(status, HasErrorMessage("Task group failed with 2 errors")); | |
| EXPECT_THAT(status, HasErrorMessage("first failure")); | |
| EXPECT_THAT(status, HasErrorMessage("second failure")); | |
| EXPECT_EQ(call_count.load(std::memory_order_relaxed), 2); | |
| EXPECT_EQ(executor.submit_count(), 2); | |
| } | |
| TEST(TaskGroupTest, ParallelSubmitErrors) { | |
| test::ThreadExecutor executor(ServiceUnavailable("executor busy")); | |
| TaskGroup group; | |
| std::atomic<int> call_count = 0; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }); | |
| group.Submit([&]() -> Status { | |
| call_count.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }); | |
| auto status = std::move(group).Run(); | |
| EXPECT_THAT(status, IsError(ErrorKind::kServiceUnavailable)); | |
| EXPECT_THAT(status, HasErrorMessage("Task group failed with 2 errors")); | |
| EXPECT_THAT(status, HasErrorMessage("executor busy")); | |
| EXPECT_EQ(call_count.load(std::memory_order_relaxed), 0); | |
| EXPECT_EQ(executor.submit_count(), 2); | |
| } | |
| TEST(TaskGroupTest, RetriesTasks) { | |
| test::FakeRetryEnvironment fake_retry; | |
| ScopedRetryTestHooks retry_hooks(fake_retry.hooks()); | |
| TaskGroup<test::TransientIORetry> group{FastRetryConfig()}; | |
| int call_count = 0; | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| if (call_count == 1) { | |
| return IOError("transient read failure"); | |
| } | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(call_count, 2); | |
| EXPECT_EQ(fake_retry.sleep_durations(), | |
| std::vector<test::FakeRetryEnvironment::Duration>( | |
| {test::FakeRetryEnvironment::Duration(1)})); | |
| } | |
| TEST(TaskGroupTest, RetryReusesTaskState) { | |
| test::FakeRetryEnvironment fake_retry; | |
| ScopedRetryTestHooks retry_hooks(fake_retry.hooks()); | |
| TaskGroup<test::TransientIORetry> group{FastRetryConfig()}; | |
| group.Submit([attempt = 0]() mutable -> Status { | |
| ++attempt; | |
| if (attempt == 1) { | |
| return IOError("transient read failure"); | |
| } | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(fake_retry.sleep_durations(), | |
| std::vector<test::FakeRetryEnvironment::Duration>( | |
| {test::FakeRetryEnvironment::Duration(1)})); | |
| } | |
| TEST(TaskGroupTest, RetryAcceptsMoveOnlyRepeatableTask) { | |
| test::FakeRetryEnvironment fake_retry; | |
| ScopedRetryTestHooks retry_hooks(fake_retry.hooks()); | |
| TaskGroup<test::TransientIORetry> group{FastRetryConfig()}; | |
| int call_count = 0; | |
| auto value = std::make_unique<int>(7); | |
| group.Submit([value = std::move(value), &call_count]() -> Status { | |
| ++call_count; | |
| if (call_count == 1) { | |
| return IOError("transient read failure"); | |
| } | |
| return *value == 7 ? Status{} : IOError("unexpected value"); | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(call_count, 2); | |
| EXPECT_EQ(fake_retry.sleep_durations(), | |
| std::vector<test::FakeRetryEnvironment::Duration>( | |
| {test::FakeRetryEnvironment::Duration(1)})); | |
| } | |
| TEST(TaskGroupTest, DefaultRetryConfig) { | |
| TaskGroup<test::TransientIORetry> group; | |
| int call_count = 0; | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(call_count, 1); | |
| } | |
| TEST(TaskGroupTest, DoesNotRetryNotFound) { | |
| TaskGroup<test::TransientIORetry> group{FastRetryConfig()}; | |
| int call_count = 0; | |
| group.Submit([&]() -> Status { | |
| ++call_count; | |
| return NotFound("missing manifest"); | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsError(ErrorKind::kNotFound)); | |
| EXPECT_EQ(call_count, 1); | |
| } | |
| TEST(TaskGroupTest, RetryUsesExecutor) { | |
| test::FakeRetryEnvironment fake_retry; | |
| ScopedRetryTestHooks retry_hooks(fake_retry.hooks()); | |
| test::ThreadExecutor executor; | |
| TaskGroup<test::TransientIORetry> group{FastRetryConfig()}; | |
| std::atomic<int> first_task_calls = 0; | |
| std::atomic<int> second_task_calls = 0; | |
| group.SetExecutor(std::ref(executor)); | |
| group.Submit([&]() -> Status { | |
| auto call_count = first_task_calls.fetch_add(1, std::memory_order_relaxed) + 1; | |
| if (call_count == 1) { | |
| return ServiceUnavailable("server busy"); | |
| } | |
| return {}; | |
| }); | |
| group.Submit([&]() -> Status { | |
| second_task_calls.fetch_add(1, std::memory_order_relaxed); | |
| return {}; | |
| }); | |
| EXPECT_THAT(std::move(group).Run(), IsOk()); | |
| EXPECT_EQ(first_task_calls.load(std::memory_order_relaxed), 2); | |
| EXPECT_EQ(second_task_calls.load(std::memory_order_relaxed), 1); | |
| EXPECT_EQ(executor.submit_count(), 2); | |
| } | |
| } // namespace iceberg |