blob: 0b04a333d746de746dad2dde900125b7de1657b1 [file]
/*
* 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