Add Operation<T> (#1186)

* Add Operation<T>
This commit is contained in:
Rick Winter 2020-12-17 14:47:20 -08:00 committed by GitHub
parent 60a18e3c3f
commit bcb684f95e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 457 additions and 2 deletions

View File

@ -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<T>`.
### Breaking Changes

View File

@ -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
)

View File

@ -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 <chrono>
#include <memory>
#include <string>
namespace Azure { namespace Core {
/**
* @brief Methods starting long-running operations return Operation<T> types.
*
* @tparam T The long-running operation final result type.
*/
template <class T> class Operation {
private:
// These are pure virtual b/c the derived class must provide an implementation
virtual std::unique_ptr<Http::RawResponse> PollInternal(Context& context) = 0;
virtual Response<T> 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<T> 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<Http::RawResponse> 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<Http::RawResponse> 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<T> the final result of the long-running operation.
*/
Response<T> 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<T> the final result of the long-running operation.
*/
Response<T> PollUntilDone(Context& context, std::chrono::milliseconds period)
{
return PollUntilDoneInternal(context, period);
}
};
}} // namespace Azure::Core

View File

@ -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 <string>
#include <utility> // 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

View File

@ -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<Http::RawResponse>&& ExtractRawResponse()
{

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include "operation_test.hpp"
#include <azure/core/context.hpp>
#include <azure/core/operation.hpp>
#include <azure/core/operation_status.hpp>
#include <chrono>
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<double, std::milli> 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);
}

View File

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include <azure/core/operation_status.hpp>
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);
}

View File

@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#pragma once
#include <azure/core/context.hpp>
#include <azure/core/nullable.hpp>
#include <azure/core/operation.hpp>
#include <azure/core/operation_status.hpp>
#include <azure/core/response.hpp>
#include <memory>
#include <string>
#include <thread>
namespace Azure { namespace Core { namespace Test {
class StringClient;
class StringOperation : public Operation<std::string> {
private:
StringClient* m_client;
std::string m_operationToken;
std::string m_value;
private:
int m_count = 0;
private:
std::unique_ptr<Http::RawResponse> 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<Http::RawResponse>(
(uint16_t)1, (uint16_t)0, Http::HttpStatusCode(200), "OK");
}
Response<std::string> PollUntilDoneInternal(Context& context, std::chrono::milliseconds period)
override
{
std::unique_ptr<Http::RawResponse> 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<std::string>(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<T> 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