From 8b7496172de3fe9fc221b7299ac42fd8b6ad546c Mon Sep 17 00:00:00 2001 From: Craig Tiller Date: Wed, 23 Oct 2024 21:09:32 -0700 Subject: [PATCH] [promises] Add a promise-based match operator (#37981) Closes #37981 COPYBARA_INTEGRATE_REVIEW=https://github.com/grpc/grpc/pull/37981 from ctiller:match-promise 8341b09901bad35f4c3fb5baed7e099aae7124f8 PiperOrigin-RevId: 689223259 --- CMakeLists.txt | 43 ++++++++++ build_autogenerated.yaml | 19 +++++ src/core/BUILD | 17 ++++ src/core/lib/promise/match_promise.h | 105 ++++++++++++++++++++++++ test/core/promise/BUILD | 18 ++++ test/core/promise/match_promise_test.cc | 66 +++++++++++++++ tools/run_tests/generated/tests.json | 24 ++++++ 7 files changed, 292 insertions(+) create mode 100644 src/core/lib/promise/match_promise.h create mode 100644 test/core/promise/match_promise_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index bbb8e9d41f5..03cda7cd03b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1629,6 +1629,7 @@ if(gRPC_BUILD_TESTS) add_dependencies(buildtests_cxx loop_test) add_dependencies(buildtests_cxx lru_cache_test) add_dependencies(buildtests_cxx map_pipe_test) + add_dependencies(buildtests_cxx match_promise_test) add_dependencies(buildtests_cxx match_test) add_dependencies(buildtests_cxx matchers_test) add_dependencies(buildtests_cxx max_concurrent_streams_test) @@ -19871,6 +19872,48 @@ target_link_libraries(map_pipe_test ) +endif() +if(gRPC_BUILD_TESTS) + +add_executable(match_promise_test + test/core/promise/match_promise_test.cc +) +if(WIN32 AND MSVC) + if(BUILD_SHARED_LIBS) + target_compile_definitions(match_promise_test + PRIVATE + "GPR_DLL_IMPORTS" + ) + endif() +endif() +target_compile_features(match_promise_test PUBLIC cxx_std_14) +target_include_directories(match_promise_test + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR} + ${_gRPC_RE2_INCLUDE_DIR} + ${_gRPC_SSL_INCLUDE_DIR} + ${_gRPC_UPB_GENERATED_DIR} + ${_gRPC_UPB_GRPC_GENERATED_DIR} + ${_gRPC_UPB_INCLUDE_DIR} + ${_gRPC_XXHASH_INCLUDE_DIR} + ${_gRPC_ZLIB_INCLUDE_DIR} + third_party/googletest/googletest/include + third_party/googletest/googletest + third_party/googletest/googlemock/include + third_party/googletest/googlemock + ${_gRPC_PROTO_GENS_DIR} +) + +target_link_libraries(match_promise_test + ${_gRPC_ALLTARGETS_LIBRARIES} + gtest + absl::type_traits + gpr +) + + endif() if(gRPC_BUILD_TESTS) diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml index 54358bb3891..f2a86992787 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -12822,6 +12822,25 @@ targets: - absl/status:statusor - gpr uses_polling: false +- name: match_promise_test + gtest: true + build: test + language: c++ + headers: + - src/core/lib/promise/detail/promise_factory.h + - src/core/lib/promise/detail/promise_like.h + - src/core/lib/promise/match_promise.h + - src/core/lib/promise/poll.h + - src/core/lib/promise/promise.h + - src/core/util/overload.h + - test/core/promise/poll_matcher.h + src: + - test/core/promise/match_promise_test.cc + deps: + - gtest + - absl/meta:type_traits + - gpr + uses_polling: false - name: match_test gtest: true build: test diff --git a/src/core/BUILD b/src/core/BUILD index 3792101db63..24a4b0ca68a 100644 --- a/src/core/BUILD +++ b/src/core/BUILD @@ -681,6 +681,23 @@ grpc_cc_library( ], ) +grpc_cc_library( + name = "match_promise", + external_deps = [ + "absl/strings", + "absl/types:variant", + ], + language = "c++", + public_hdrs = ["lib/promise/match_promise.h"], + deps = [ + "overload", + "poll", + "promise_factory", + "promise_like", + "//:gpr_platform", + ], +) + grpc_cc_library( name = "sleep", srcs = [ diff --git a/src/core/lib/promise/match_promise.h b/src/core/lib/promise/match_promise.h new file mode 100644 index 00000000000..eeb6152a18c --- /dev/null +++ b/src/core/lib/promise/match_promise.h @@ -0,0 +1,105 @@ +// Copyright 2024 gRPC authors. +// +// 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. + +#ifndef GRPC_SRC_CORE_LIB_PROMISE_MATCH_PROMISE_H +#define GRPC_SRC_CORE_LIB_PROMISE_MATCH_PROMISE_H + +#include "absl/types/variant.h" +#include "src/core/lib/promise/detail/promise_factory.h" +#include "src/core/lib/promise/detail/promise_like.h" +#include "src/core/util/overload.h" + +namespace grpc_core { + +namespace promise_detail { + +// This types job is to visit a supplied variant, and apply a mapping +// Constructor from input types to promises, returning a variant full of +// promises. +template +struct ConstructPromiseVariantVisitor { + // Factory functions supplied to the top level `Match` object, wrapped by + // OverloadType to become overloaded members. + Constructor constructor; + + // Helper function... only callable once. + // Given a value, construct a Promise Factory that accepts that value type, + // and uses the constructor type above to map from that type to a promise + // returned by the factory. + // We use the Promise Factory infrastructure to deal with all the common + // variants of factory signatures that we've found to be convenient. + template + auto CallConstructorThenFactory(T x) { + OncePromiseFactory factory(std::move(constructor)); + return factory.Make(std::move(x)); + } + + // Polling operator. + // Given a visited type T, construct a Promise Factory, use it, and then cast + // the result into a variant type that covers ALL of the possible return types + // given the input types listed in Ts... + template + auto operator()(T x) + -> absl::variant()))>...> { + return CallConstructorThenFactory(x); + } +}; + +// Visitor function for PromiseVariant - calls the poll operator on the inner +// type +class PollVisitor { + public: + template + auto operator()(T& x) { + return x(); + } +}; + +// Helper type - given a variant V, provides the poll operator (which simply +// visits the inner type on the variant with PollVisitor) +template +class PromiseVariant { + public: + explicit PromiseVariant(V variant) : variant_(std::move(variant)) {} + auto operator()() { return absl::visit(PollVisitor(), variant_); } + + private: + V variant_; +}; + +} // namespace promise_detail + +// Match for promises +// Like the Match function takes a variant of some set of types, +// and a set of functions - one per variant type. +// We use these functions as Promise Factories, and return a Promise that can be +// polled selected by the type that was in the variant. +template +auto MatchPromise(absl::variant value, Fs... fs) { + // Construct a variant of promises using the factory functions fs, selected by + // the type held by value. + auto body = absl::visit( + promise_detail::ConstructPromiseVariantVisitor, + Ts...>{ + OverloadType(std::move(fs)...)}, + std::move(value)); + // Wrap that in a PromiseVariant that provides the promise API on the wrapped + // variant. + return promise_detail::PromiseVariant(std::move(body)); +} + +} // namespace grpc_core + +#endif // GRPC_SRC_CORE_LIB_PROMISE_MATCH_PROMISE_H diff --git a/test/core/promise/BUILD b/test/core/promise/BUILD index 1d919291be5..68f813a5912 100644 --- a/test/core/promise/BUILD +++ b/test/core/promise/BUILD @@ -152,6 +152,24 @@ grpc_cc_test( ], ) +grpc_cc_test( + name = "match_promise_test", + srcs = ["match_promise_test.cc"], + external_deps = [ + "absl/functional:any_invocable", + "gtest", + ], + language = "c++", + tags = ["promise_test"], + uses_event_engine = False, + uses_polling = False, + deps = [ + "poll_matcher", + "//:promise", + "//src/core:match_promise", + ], +) + grpc_cc_test( name = "race_test", srcs = ["race_test.cc"], diff --git a/test/core/promise/match_promise_test.cc b/test/core/promise/match_promise_test.cc new file mode 100644 index 00000000000..f948bd6fbe9 --- /dev/null +++ b/test/core/promise/match_promise_test.cc @@ -0,0 +1,66 @@ +// Copyright 2021 gRPC authors. +// +// 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 "src/core/lib/promise/match_promise.h" + +#include + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" +#include "src/core/lib/promise/promise.h" +#include "test/core/promise/poll_matcher.h" + +namespace grpc_core { + +TEST(MatchPromiseTest, Works) { + struct Int { + int x; + }; + struct Float { + float x; + }; + using V = absl::variant; + auto make_promise = [](V v) -> Promise { + return MatchPromise( + std::move(v), + [](Float x) mutable { + return [n = 3, x]() mutable -> Poll { + --n; + if (n > 0) return Pending{}; + return absl::StrCat(x.x); + }; + }, + [](Int x) { + return []() mutable -> Poll { return Pending{}; }; + }, + [](std::string x) { return x; }); + }; + auto promise = make_promise(V(Float{3.0f})); + EXPECT_THAT(promise(), IsPending()); + EXPECT_THAT(promise(), IsPending()); + EXPECT_THAT(promise(), IsReady("3")); + promise = make_promise(V(Int{42})); + for (int i = 0; i < 10000; i++) { + EXPECT_THAT(promise(), IsPending()); + } + promise = make_promise(V("hello")); + EXPECT_THAT(promise(), IsReady("hello")); +} + +} // namespace grpc_core + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json index 5301d9c1084..03bcb4953ba 100644 --- a/tools/run_tests/generated/tests.json +++ b/tools/run_tests/generated/tests.json @@ -5753,6 +5753,30 @@ ], "uses_polling": false }, + { + "args": [], + "benchmark": false, + "ci_platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "cpu_cost": 1.0, + "exclude_configs": [], + "exclude_iomgrs": [], + "flaky": false, + "gtest": true, + "language": "c++", + "name": "match_promise_test", + "platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "uses_polling": false + }, { "args": [], "benchmark": false,