From bcb684f95ed1641017fded01d08cd0f64e83e3a2 Mon Sep 17 00:00:00 2001 From: Rick Winter Date: Thu, 17 Dec 2020 14:47:20 -0800 Subject: [PATCH] Add Operation (#1186) * Add Operation --- sdk/core/azure-core/CHANGELOG.md | 1 + sdk/core/azure-core/CMakeLists.txt | 3 + .../azure-core/inc/azure/core/operation.hpp | 125 ++++++++++++++++++ .../inc/azure/core/operation_status.hpp | 78 +++++++++++ .../azure-core/inc/azure/core/response.hpp | 2 +- sdk/core/azure-core/src/operation_status.cpp | 14 ++ sdk/core/azure-core/test/ut/CMakeLists.txt | 4 +- sdk/core/azure-core/test/ut/operation.cpp | 90 +++++++++++++ .../azure-core/test/ut/operation_status.cpp | 51 +++++++ .../azure-core/test/ut/operation_test.hpp | 91 +++++++++++++ 10 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 sdk/core/azure-core/inc/azure/core/operation.hpp create mode 100644 sdk/core/azure-core/inc/azure/core/operation_status.hpp create mode 100644 sdk/core/azure-core/src/operation_status.cpp create mode 100644 sdk/core/azure-core/test/ut/operation.cpp create mode 100644 sdk/core/azure-core/test/ut/operation_status.cpp create mode 100644 sdk/core/azure-core/test/ut/operation_test.hpp diff --git a/sdk/core/azure-core/CHANGELOG.md b/sdk/core/azure-core/CHANGELOG.md index 41d1a9a54..1c527f42f 100644 --- a/sdk/core/azure-core/CHANGELOG.md +++ b/sdk/core/azure-core/CHANGELOG.md @@ -6,6 +6,7 @@ - Added a WinHTTP-based `HttpTransport` called `WinHttpTransport` and use that as the default `TransportPolicyOptions.Transport` on Windows when sending and receiving requests and responses over the wire. - Added `Range` type to `Azure::Core::Http` namespace. +- Added support for long-running operations with `Operation`. ### Breaking Changes diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index b1c2148ec..794ede98b 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -67,6 +67,8 @@ set( inc/azure/core/datetime.hpp inc/azure/core/exception.hpp inc/azure/core/nullable.hpp + inc/azure/core/operation.hpp + inc/azure/core/operation_status.hpp inc/azure/core/platform.hpp inc/azure/core/response.hpp inc/azure/core/strings.hpp @@ -93,6 +95,7 @@ set( src/logging/logging.cpp src/context.cpp src/datetime.cpp + src/operation_status.cpp src/strings.cpp src/version.cpp ) diff --git a/sdk/core/azure-core/inc/azure/core/operation.hpp b/sdk/core/azure-core/inc/azure/core/operation.hpp new file mode 100644 index 000000000..24e76402f --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/operation.hpp @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Provides support for long-running operations. + */ + +#pragma once + +#include "azure/core/context.hpp" +#include "azure/core/operation_status.hpp" +#include "azure/core/response.hpp" + +#include +#include +#include + +namespace Azure { namespace Core { + + /** + * @brief Methods starting long-running operations return Operation types. + * + * @tparam T The long-running operation final result type. + */ + template class Operation { + private: + // These are pure virtual b/c the derived class must provide an implementation + virtual std::unique_ptr PollInternal(Context& context) = 0; + virtual Response PollUntilDoneInternal(Context& context, std::chrono::milliseconds period) + = 0; + + protected: + OperationStatus m_status = OperationStatus::NotStarted; + + public: + /** + * @brief Final reuslt of the long-running operation. + * + * @return Response the final result of the long-running operation. + */ + virtual T Value() const = 0; + + /** + * @brief Gets an token representing the operation that can be used to poll for the status of + * the long-running operation. + * + * @return std::string The resume token. + */ + virtual std::string GetResumeToken() const = 0; + + /** + * @brief Returns the current #OperationStatus of the long-running operation. + */ + OperationStatus Status() const noexcept { return m_status; } + + /** + * @brief Returns true if the long-running operation completed. + * + * @return `true` if the long-running operation is done. `false` otherwise. + */ + bool IsDone() const noexcept + { + return ( + m_status == OperationStatus::Succeeded || m_status == OperationStatus::Cancelled + || m_status == OperationStatus::Failed); + } + + /** + * @brief Returns true if the long-running operation completed successfully and has produced a + * final result. The final result is accessible from Value(). + * + * @return `true` if the long-running operation completed successfully. `false` otherwise. + */ + bool HasValue() const noexcept { return (m_status == OperationStatus::Succeeded); } + + /** + * @brief Calls the server to get updated status of the long-running operation. + * + * @return An HTTP #RawResponse returned from the service. + */ + std::unique_ptr Poll() + { + // In the cases where the customer doesn't want to use a context we new one up and pass it + // through + return PollInternal(GetApplicationContext()); + } + + /** + * @brief Calls the server to get updated status of the long-running operation. + * + * @param context #Context allows the user to cancel the long-running operation. + * + * @return An HTTP #RawResponse returned from the service. + */ + std::unique_ptr Poll(Context& context) { return PollInternal(context); } + + /** + * @brief Periodically calls the server till the long-running operation completes; + * + * @param period Time in milliseconds to wait between polls + * + * @return Response the final result of the long-running operation. + */ + Response PollUntilDone(std::chrono::milliseconds period) + { + // In the cases where the customer doesn't want to use a context we new one up and pass it + // through + return PollUntilDoneInternal(GetApplicationContext(), period); + } + + /** + * @brief Periodically calls the server till the long-running operation completes; + * + * @param context #Context allows the user to cancel the long-running operation. + * @param period Time in milliseconds to wait between polls + * + * @return Response the final result of the long-running operation. + */ + Response PollUntilDone(Context& context, std::chrono::milliseconds period) + { + return PollUntilDoneInternal(context, period); + } + }; +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/inc/azure/core/operation_status.hpp b/sdk/core/azure-core/inc/azure/core/operation_status.hpp new file mode 100644 index 000000000..1761a9880 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/operation_status.hpp @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Valid states for long-running Operations. Services can extend upon the default set of + * values. + */ + +#pragma once + +#include +#include // for std::move + +#include "azure/core/strings.hpp" + +namespace Azure { namespace Core { + + /** + * @brief Long-running operation states. + */ + class OperationStatus { + std::string m_value; + + public: + /** + * @brief Construct a #OperationStatus with \p value. + * + * @param value A non-absent value to initialize with. + */ + explicit OperationStatus(const std::string& value) : m_value(value) {} + /** + * @brief Construct a #OperationStatus with \p value. + * + * @param value A non-absent value to initialize with. + */ + explicit OperationStatus(std::string&& value) : m_value(std::move(value)) {} + /** + * @brief Construct a #OperationStatus with \p value. + * + * @param value A non-absent value to initialize with. + */ + explicit OperationStatus(const char* value) : m_value(value) {} + + /** + * @brief Compare two #OperationStatus objects + * + * @param other A #OperationStatus to compare with. + * + * @return `true` if the states have the same string representation. `false` otherwise. + */ + bool operator==(const OperationStatus& other) const noexcept + { + return Strings::LocaleInvariantCaseInsensitiveEqual(m_value, other.m_value); + } + + /** + * @brief Compare two #OperationStatus objects + * + * @param other A #OperationStatus to compare with. + * + * @return `false` if the states have the same string representation. `true` otherwise. + */ + bool operator!=(const OperationStatus& other) const noexcept { return !(*this == other); } + + /** + * @brief The std::string representation of the value + */ + const std::string& Get() const noexcept { return m_value; } + + static const OperationStatus NotStarted; + static const OperationStatus Running; + static const OperationStatus Succeeded; + static const OperationStatus Cancelled; + static const OperationStatus Failed; + }; + +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/inc/azure/core/response.hpp b/sdk/core/azure-core/inc/azure/core/response.hpp index 218ebc7bc..b74830950 100644 --- a/sdk/core/azure-core/inc/azure/core/response.hpp +++ b/sdk/core/azure-core/inc/azure/core/response.hpp @@ -97,7 +97,7 @@ namespace Azure { namespace Core { } /** - * @brief Get a smaprt pointer rvalue reference to the value of a specific type. + * @brief Get a smart pointer rvalue reference to the value of a specific type. */ std::unique_ptr&& ExtractRawResponse() { diff --git a/sdk/core/azure-core/src/operation_status.cpp b/sdk/core/azure-core/src/operation_status.cpp new file mode 100644 index 000000000..5acc2d860 --- /dev/null +++ b/sdk/core/azure-core/src/operation_status.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/core/operation_status.hpp" + +namespace Azure { namespace Core { + + const OperationStatus OperationStatus::NotStarted("NotStarted"); + const OperationStatus OperationStatus::Running{"Running"}; + const OperationStatus OperationStatus::Succeeded{"Succeeded"}; + const OperationStatus OperationStatus::Failed{"Failed"}; + const OperationStatus OperationStatus::Cancelled{"Cancelled"}; + +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index da2614218..03831a70e 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -42,8 +42,10 @@ add_executable ( logging.cpp main.cpp nullable.cpp + operation.cpp + operation_status.cpp pipeline.cpp - policy.cpp + policy.cpp simplified_header.cpp string.cpp telemetry_policy.cpp diff --git a/sdk/core/azure-core/test/ut/operation.cpp b/sdk/core/azure-core/test/ut/operation.cpp new file mode 100644 index 000000000..eee65274a --- /dev/null +++ b/sdk/core/azure-core/test/ut/operation.cpp @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include "operation_test.hpp" + +#include +#include +#include + +#include + +using namespace Azure::Core; +using namespace Azure::Core::Test; +using namespace std::literals; + +TEST(Operation, Poll) +{ + StringClient client; + auto operation = client.StartStringUpdate(); + + EXPECT_FALSE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + + while(!operation.IsDone()) + { + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + auto response = operation.Poll(); + } + + EXPECT_TRUE(operation.IsDone()); + EXPECT_TRUE(operation.HasValue()); + + auto result = operation.Value(); + EXPECT_TRUE(result == "StringOperation-Completed"); +} + +TEST(Operation, PollUntilDone) +{ + StringClient client; + auto operation = client.StartStringUpdate(); + + EXPECT_FALSE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + + auto start = std::chrono::high_resolution_clock::now(); + auto response = operation.PollUntilDone(500ms); + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end - start; + //StringOperation test code is implemented to poll 2 times + EXPECT_TRUE(elapsed >= 1s); + + EXPECT_TRUE(operation.IsDone()); + EXPECT_TRUE(operation.HasValue()); + + auto result = operation.Value(); + EXPECT_EQ(result, "StringOperation-Completed"); +} + +TEST(Operation, Status) +{ + StringClient client; + auto operation = client.StartStringUpdate(); + + EXPECT_FALSE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + EXPECT_EQ(operation.Status(), OperationStatus::NotStarted); + + operation.SetOperationStatus(OperationStatus::Running); + EXPECT_FALSE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + EXPECT_EQ(operation.Status(), OperationStatus::Running); + + operation.SetOperationStatus(OperationStatus::Failed); + EXPECT_TRUE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + EXPECT_EQ(operation.Status(), OperationStatus::Failed); + + operation.SetOperationStatus(OperationStatus::Cancelled); + EXPECT_TRUE(operation.IsDone()); + EXPECT_FALSE(operation.HasValue()); + EXPECT_THROW(operation.Value(), std::runtime_error); + EXPECT_EQ(operation.Status(), OperationStatus::Cancelled); +} diff --git a/sdk/core/azure-core/test/ut/operation_status.cpp b/sdk/core/azure-core/test/ut/operation_status.cpp new file mode 100644 index 000000000..029f1a228 --- /dev/null +++ b/sdk/core/azure-core/test/ut/operation_status.cpp @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include + +using namespace Azure::Core; + +TEST(OperationStatus, Basic) +{ + OperationStatus status = OperationStatus::Cancelled; + EXPECT_EQ(status, OperationStatus::Cancelled); + EXPECT_EQ(status.Get(), "Cancelled"); + + status = OperationStatus::Failed; + EXPECT_EQ(status, OperationStatus::Failed); + EXPECT_EQ(status.Get(), "Failed"); + + status = OperationStatus::NotStarted; + EXPECT_EQ(status, OperationStatus::NotStarted); + EXPECT_EQ(status.Get(), "NotStarted"); + + status = OperationStatus::Running; + EXPECT_EQ(status, OperationStatus::Running); + EXPECT_EQ(status.Get(), "Running"); + + status = OperationStatus::Succeeded; + EXPECT_EQ(status, OperationStatus::Succeeded); + EXPECT_EQ(status.Get(), "Succeeded"); +} + +TEST(OperationStatus, Custom) +{ + OperationStatus status1("CustomValue"); + EXPECT_EQ(status1.Get(), "CustomValue"); + EXPECT_NE(status1, OperationStatus::NotStarted); + + OperationStatus status2 = OperationStatus("CustomValue"); + EXPECT_EQ(status2.Get(), "CustomValue"); + EXPECT_NE(status2, OperationStatus::NotStarted); + + std::string custom("CustomValue"); + OperationStatus status3 = OperationStatus(custom); + EXPECT_EQ(status3.Get(), custom); + EXPECT_NE(status3, OperationStatus::NotStarted); + + OperationStatus status4 = OperationStatus(std::string("CustomValue")); + EXPECT_EQ(status4.Get(), "CustomValue"); + EXPECT_NE(status4, OperationStatus::NotStarted); +} diff --git a/sdk/core/azure-core/test/ut/operation_test.hpp b/sdk/core/azure-core/test/ut/operation_test.hpp new file mode 100644 index 000000000..4d440e7ca --- /dev/null +++ b/sdk/core/azure-core/test/ut/operation_test.hpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Azure { namespace Core { namespace Test { + + class StringClient; + + class StringOperation : public Operation { + + private: + StringClient* m_client; + std::string m_operationToken; + std::string m_value; + + private: + int m_count = 0; + + private: + std::unique_ptr PollInternal(Context& context) override + { + // Artificial delay to require 2 polls + if (++m_count == 2) + { + m_status = OperationStatus::Succeeded; + m_value = "StringOperation-Completed"; + } + + // The contents of the response are irrelevant for testing purposes + // Need only ensure that a RawResponse is returned + return std::make_unique( + (uint16_t)1, (uint16_t)0, Http::HttpStatusCode(200), "OK"); + } + + Response PollUntilDoneInternal(Context& context, std::chrono::milliseconds period) + override + { + std::unique_ptr response; + while (!IsDone()) + { + // Sleep for the period + // Actual clients should respect the retry after header if present + std::this_thread::sleep_for(period); + + response = Poll(context); + } + + return Response(m_value, std::move(response)); + } + + public: + StringOperation(StringClient* client) : m_client(client) {} + + std::string GetResumeToken() const override { return m_operationToken; } + + std::string Value() const override + { + if (m_status != OperationStatus::Succeeded) + { + throw std::runtime_error("InvalidOperation"); + } + + return m_value; + } + + // This is a helper method to allow testing of the underlying operation behaviors + // ClientOperations would not expose a way to control status + void SetOperationStatus(OperationStatus status) { m_status = status; } + }; + + class StringClient { + public: + StringOperation StartStringUpdate() + { + // Make initial String call + StringOperation operation = StringOperation(this); + return operation; + } + }; + +}}} // namespace Azure::Core::Test