From 18531a1f9cb014aace6ffeceb91fe9ec452a810e Mon Sep 17 00:00:00 2001 From: Dan Schulte Date: Tue, 19 Sep 2017 14:17:59 -0700 Subject: [PATCH 1/3] Add support for Location and Azure-AsyncOperation long running operations --- .../v2/AzureAsyncOperationPollStrategy.java | 160 ++++++++++++ .../com/microsoft/azure/v2/AzureProxy.java | 181 ++++++------- .../azure/v2/LocationPollStrategy.java | 84 +++++++ .../microsoft/azure/v2/OperationResource.java | 59 +++++ .../microsoft/azure/v2/OperationStatus.java | 132 ++++++++-- .../com/microsoft/azure/v2/PollStrategy.java | 77 ++++++ .../microsoft/azure/v2/AzureProxyTests.java | 238 +++++++++--------- .../azure/v2/http/MockAzureHttpClient.java | 132 ++++++---- .../java/com/microsoft/rest/v2/RestProxy.java | 20 +- 9 files changed, 774 insertions(+), 309 deletions(-) create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationResource.java create mode 100644 azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java new file mode 100644 index 0000000000000..2a6691563860e --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.v2; + +import com.microsoft.rest.protocol.SerializerAdapter; +import com.microsoft.rest.v2.http.HttpRequest; +import com.microsoft.rest.v2.http.HttpResponse; +import rx.Single; +import rx.functions.Func1; + +import java.io.IOException; + +/** + * A PollStrategy type that uses the Azure-AsyncOperation header value to check the status of a long + * running operation. + */ +public final class AzureAsyncOperationPollStrategy extends PollStrategy { + private final String fullyQualifiedMethodName; + private final String operationResourceUrl; + private final String originalResourceUrl; + private final SerializerAdapter serializer; + private Boolean pollingSucceeded; + private boolean gotResourceResponse; + + /** + * The name of the header that indicates that a long running operation will use the + * Azure-AsyncOperation strategy. + */ + public static final String HEADER_NAME = "Azure-AsyncOperation"; + + /** + * The provisioning state of the operation resource if the operation is still in progress. + */ + public static final String IN_PROGRESS = "InProgress"; + + /** + * The provisioning state of the operation resource if the operation is successful. + */ + public static final String SUCCEEDED = "Succeeded"; + + /** + * Create a new AzureAsyncOperationPollStrategy object that will poll the provided operation + * resource URL. + * @param fullyQualifiedMethodName The fully qualified name of the method that initiated the + * long running operation. + * @param operationResourceUrl The URL of the operation resource this pollStrategy will poll. + * @param serializer The serializer that will deserialize the operation resource and the + * final operation result. + */ + private AzureAsyncOperationPollStrategy(String fullyQualifiedMethodName, String operationResourceUrl, String originalResourceUrl, SerializerAdapter serializer) { + this.fullyQualifiedMethodName = fullyQualifiedMethodName; + this.operationResourceUrl = operationResourceUrl; + this.originalResourceUrl = originalResourceUrl; + this.serializer = serializer; + } + + @Override + public HttpRequest createPollRequest() { + String pollUrl = null; + if (pollingSucceeded == null) { + pollUrl = operationResourceUrl; + } + else if (pollingSucceeded) { + pollUrl = originalResourceUrl; + } + return new HttpRequest(fullyQualifiedMethodName, "GET", pollUrl); + } + + @Override + public void updateFrom(HttpResponse httpPollResponse) throws IOException { + updateDelayInMillisecondsFrom(httpPollResponse); + + if (pollingSucceeded == null) { + final String bodyString = httpPollResponse.bodyAsString(); + updateFromResponseBodyString(bodyString); + } + else if (pollingSucceeded) { + gotResourceResponse = true; + } + } + + @Override + public Single updateFromAsync(final HttpResponse httpPollResponse) { + Single result; + + if (pollingSucceeded == null) { + updateDelayInMillisecondsFrom(httpPollResponse); + + result = httpPollResponse.bodyAsStringAsync() + .flatMap(new Func1>() { + @Override + public Single call(String bodyString) { + Single result = Single.just(httpPollResponse); + try { + updateFromResponseBodyString(bodyString); + } catch (IOException e) { + result = Single.error(e); + } + return result; + } + }); + } + else { + if (pollingSucceeded) { + gotResourceResponse = true; + } + + result = Single.just(httpPollResponse); + } + + return result; + } + + private void updateFromResponseBodyString(String httpResponseBodyString) throws IOException { + final OperationResource operationResource = serializer.deserialize(httpResponseBodyString, OperationResource.class); + if (operationResource != null) { + final String provisioningState = provisioningState(operationResource); + if (!IN_PROGRESS.equalsIgnoreCase(provisioningState)) { + pollingSucceeded = SUCCEEDED.equalsIgnoreCase(provisioningState); + clearDelayInMilliseconds(); + } + } + } + + private static String provisioningState(OperationResource operationResource) { + String provisioningState = null; + + final OperationResource.Properties properties = operationResource.properties(); + if (properties != null) { + provisioningState = properties.provisioningState(); + } + + return provisioningState; + } + + @Override + public boolean isDone() { + return pollingSucceeded != null && (!pollingSucceeded || gotResourceResponse); + } + + /** + * Try to create a new AzureAsyncOperationPollStrategy object that will poll the provided + * operation resource URL. If the provided HttpResponse doesn't have an Azure-AsyncOperation + * header or if the header is empty, then null will be returned. + * @param fullyQualifiedMethodName The fully qualified name of the method that initiated the + * long running operation. + * @param httpResponse The HTTP response that the required header values for this pollStrategy + * will be read from. + */ + static AzureAsyncOperationPollStrategy tryToCreate(String fullyQualifiedMethodName, HttpResponse httpResponse, String originalResourceUrl, SerializerAdapter serializer) { + final String azureAsyncOperationUrl = httpResponse.headerValue(HEADER_NAME); + return azureAsyncOperationUrl != null && !azureAsyncOperationUrl.isEmpty() + ? new AzureAsyncOperationPollStrategy(fullyQualifiedMethodName, azureAsyncOperationUrl, originalResourceUrl, serializer) + : null; + } +} \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java index 333752b6afc37..1dbc94ff1da62 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java @@ -25,13 +25,14 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; -import java.util.concurrent.TimeUnit; /** * This class can be used to create an Azure specific proxy implementation for a provided Swagger * generated interface. */ -public class AzureProxy extends RestProxy { +final class AzureProxy extends RestProxy { + private static long defaultDelayInMilliseconds = 30 * 1000; + /** * Create a new instance of RestProxy. * @param httpClient The HttpClient that will be used by this RestProxy to send HttpRequests. @@ -39,10 +40,28 @@ public class AzureProxy extends RestProxy { * @param interfaceParser The parser that contains information about the swagger interface that * this RestProxy "implements". */ - AzureProxy(HttpClient httpClient, SerializerAdapter serializer, SwaggerInterfaceParser interfaceParser) { + private AzureProxy(HttpClient httpClient, SerializerAdapter serializer, SwaggerInterfaceParser interfaceParser) { super(httpClient, serializer, interfaceParser); } + /** + * Get the millisecond delay that will occur by default between long running operation polls. + * @return The millisecond delay that will occur by default between long running operation + * polls. + */ + public static long defaultDelayInMilliseconds() { + return AzureProxy.defaultDelayInMilliseconds; + } + + /** + * Set the millisecond delay that will occur by default between long running operation polls. + * @param defaultDelayInMilliseconds The number of milliseconds to delay before sending the next + * long running operation status poll. + */ + public static void setDefaultDelayInMilliseconds(long defaultDelayInMilliseconds) { + AzureProxy.defaultDelayInMilliseconds = defaultDelayInMilliseconds; + } + /** * Create a proxy implementation of the provided Swagger interface. * @param swaggerInterface The Swagger interface to provide a proxy implementation for. @@ -63,26 +82,26 @@ public static A create(Class swaggerInterface, final HttpClient httpClien } @Override - protected Object handleSyncHttpResponse(HttpResponse httpResponse, SwaggerMethodParser methodParser) throws IOException, InterruptedException { - String pollUrl = null; - Long retryAfterSeconds = null; - while (!isDonePolling(httpResponse)) { - pollUrl = getPollUrl(httpResponse, pollUrl); + protected Object handleSyncHttpResponse(HttpRequest httpRequest, HttpResponse httpResponse, SwaggerMethodParser methodParser) throws IOException, InterruptedException { + final SerializerAdapter serializer = serializer(); - retryAfterSeconds = getRetryAfterSeconds(httpResponse, retryAfterSeconds); - if (retryAfterSeconds != null && retryAfterSeconds > 0) { - Thread.sleep(retryAfterSeconds * 1000); - } + final OperationStatus operationStatus = new OperationStatus<>(httpRequest, httpResponse, serializer); + while (!operationStatus.isDone()) { + operationStatus.delay(); - final HttpRequest pollRequest = createPollRequest(methodParser.fullyQualifiedMethodName(), pollUrl); + final HttpRequest pollRequest = operationStatus.createPollRequest(); httpResponse = sendHttpRequest(pollRequest); + + operationStatus.updateFrom(httpResponse); } - return super.handleSyncHttpResponse(httpResponse, methodParser); + return super.handleSyncHttpResponse(httpRequest, httpResponse, methodParser); } @Override - protected Object handleAsyncHttpResponse(Single asyncHttpResponse, final SwaggerMethodParser methodParser) { + protected Object handleAsyncHttpResponse(final HttpRequest httpRequest, Single asyncHttpResponse, final SwaggerMethodParser methodParser) { + final SerializerAdapter serializer = serializer(); + Object result = null; final Type returnType = methodParser.returnType(); @@ -92,23 +111,20 @@ protected Object handleAsyncHttpResponse(Single asyncHttpResponse, asyncHttpResponse = asyncHttpResponse .flatMap(new Func1>() { @Override - public Single call(HttpResponse response) { + public Single call(HttpResponse httpResponse) { + final OperationStatus operationStatus = new OperationStatus<>(httpRequest, httpResponse, serializer); + Single result; - if (isDonePolling(response)) { - result = Single.just(response); + if (operationStatus.isDone()) { + result = Single.just(httpResponse); } else { - final Value pollUrl = new Value<>(getPollUrl(response, null)); - final Value retryAfterSeconds = new Value<>(getRetryAfterSeconds(response, null)); - - result = sendPollRequestWithDelay(methodParser, pollUrl, retryAfterSeconds) + result = sendPollRequestWithDelay(operationStatus) .repeat() .takeUntil(new Func1() { @Override - public Boolean call(HttpResponse response) { - pollUrl.set(getPollUrl(response, pollUrl.get())); - retryAfterSeconds.set(getRetryAfterSeconds(response, retryAfterSeconds.get())); - return isDonePolling(response); + public Boolean call(HttpResponse ignored) { + return operationStatus.isDone(); } }) .last() @@ -117,7 +133,7 @@ public Boolean call(HttpResponse response) { return result; } }); - result = super.handleAsyncHttpResponse(asyncHttpResponse, methodParser); + result = super.handleAsyncHttpResponse(httpRequest, asyncHttpResponse, methodParser); } else if (returnTypeToken.isSubtypeOf(Observable.class)) { final Type operationStatusType = ((ParameterizedType) returnType).getActualTypeArguments()[0]; @@ -131,28 +147,23 @@ else if (returnTypeToken.isSubtypeOf(Observable.class)) { .toObservable() .flatMap(new Func1>>() { @Override - public Observable> call(HttpResponse response) { + public Observable> call(HttpResponse httpResponse) { + final OperationStatus operationStatus = new OperationStatus<>(httpRequest, httpResponse, serializer); + Observable> result; - if (isDonePolling(response)) { - return toOperationStatusObservable(response, methodParser, operationStatusResultType); + if (operationStatus.isDone()) { + result = toCompletedOperationStatusObservable(operationStatus, httpRequest, httpResponse, methodParser, operationStatusResultType); } else { - final Value pollUrl = new Value<>(getPollUrl(response, null)); - final Value retryAfterSeconds = new Value<>(getRetryAfterSeconds(response, null)); - final Value> lastOperationStatus = new Value<>(); - - result = sendPollRequestWithDelay(methodParser, pollUrl, retryAfterSeconds) + result = sendPollRequestWithDelay(operationStatus) .flatMap(new Func1>>() { @Override public Observable> call(HttpResponse httpResponse) { - pollUrl.set(getPollUrl(httpResponse, pollUrl.get())); - retryAfterSeconds.set(getRetryAfterSeconds(httpResponse, retryAfterSeconds.get())); - Observable> result; - if (isDonePolling(httpResponse)) { - result = toOperationStatusObservable(httpResponse, methodParser, operationStatusResultType); + if (!operationStatus.isDone()) { + result = Observable.just(operationStatus); } else { - result = Observable.just(OperationStatus.inProgress()); + result = toCompletedOperationStatusObservable(operationStatus, httpRequest, httpResponse, methodParser, operationStatusResultType); } return result; } @@ -161,21 +172,13 @@ public Observable> call(HttpResponse httpResponse) { .takeUntil(new Func1, Boolean>() { @Override public Boolean call(OperationStatus operationStatus) { - final boolean stop = operationStatus.isDone(); - if (stop) { - // Take until will not return the operationStatus that is - // marked as done, so we set it here and then concatWith() it - // later to force the Observable to return the operationStatus - // that is done. - lastOperationStatus.set(operationStatus); - } - return stop; + return operationStatus.isDone(); } }) .concatWith(Observable.defer(new Func0>>() { @Override public Observable> call() { - return Observable.just(lastOperationStatus.get()); + return Observable.just(operationStatus); } })); } @@ -188,11 +191,11 @@ public Observable> call() { return result; } - private Observable> toOperationStatusObservable(HttpResponse httpResponse, SwaggerMethodParser methodParser, Type operationStatusResultType) { + private Observable> toCompletedOperationStatusObservable(OperationStatus operationStatus, HttpRequest httpRequest, HttpResponse httpResponse, SwaggerMethodParser methodParser, Type operationStatusResultType) { Observable> result; try { - final Object resultObject = super.handleSyncHttpResponse(httpResponse, methodParser, operationStatusResultType); - final OperationStatus operationStatus = OperationStatus.completed(resultObject); + final Object resultObject = super.handleSyncHttpResponse(httpRequest, httpResponse, methodParser, operationStatusResultType); + operationStatus.setResult(resultObject); result = Observable.just(operationStatus); } catch (IOException e) { result = Observable.error(e); @@ -200,63 +203,27 @@ private Observable> toOperationStatusObservable(HttpResp return result; } - /** - * Get the URL from the provided HttpResponse that should be requested in order to poll the - * status of a long running operation. - * @param response The HttpResponse that contains the poll URL. - * @param currentPollUrl The poll URL that was previously used. Sometimes a HttpResponse may not - * have an updated poll URL, so in those cases we should just use the - * previous one. - * @return The URL that should be requested in order to poll the status of a long running - * operation. - */ - static String getPollUrl(HttpResponse response, String currentPollUrl) { - String pollUrl = currentPollUrl; - - final String location = response.headerValue("Location"); - if (location != null && !location.isEmpty()) { - pollUrl = location; - } - - return pollUrl; - } - - private Observable sendPollRequestWithDelay(SwaggerMethodParser methodParser, final Value pollUrl, final Value retryAfterSeconds) { - final String fullyQualifiedMethodName = methodParser.fullyQualifiedMethodName(); - + private Observable sendPollRequestWithDelay(final OperationStatus operationStatus) { return Observable.defer(new Func0>() { @Override public Observable call() { - final HttpRequest pollRequest = createPollRequest(fullyQualifiedMethodName, pollUrl.get()); - - Single pollResponse = sendHttpRequestAsync(pollRequest); - if (retryAfterSeconds.get() != null) { - pollResponse = pollResponse.delay(retryAfterSeconds.get(), TimeUnit.SECONDS); - } - return pollResponse.toObservable(); + return operationStatus + .delayAsync() + .flatMap(new Func1>() { + @Override + public Single call(Void ignored) { + final HttpRequest pollRequest = operationStatus.createPollRequest(); + return sendHttpRequestAsync(pollRequest); + } + }) + .flatMap(new Func1>() { + @Override + public Single call(HttpResponse response) { + return operationStatus.updateFromAsync(response); + } + }) + .toObservable(); } }); } - - private static HttpRequest createPollRequest(String fullyQualifiedMethodName, String pollUrl) { - return new HttpRequest(fullyQualifiedMethodName, "GET", pollUrl); - } - - private static boolean isDonePolling(HttpResponse response) { - return response.statusCode() != 202; - } - - static Long getRetryAfterSeconds(HttpResponse response, Long currentRetryAfterSeconds) { - Long retryAfterSeconds = currentRetryAfterSeconds; - - final String retryAfterSecondsString = response.headerValue("Retry-After"); - if (retryAfterSecondsString != null && !retryAfterSecondsString.isEmpty()) { - try { - retryAfterSeconds = Long.valueOf(retryAfterSecondsString); - } catch (Exception ignored) { - } - } - - return retryAfterSeconds; - } } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java new file mode 100644 index 0000000000000..5bad92b2dcb3e --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.v2; + +import com.microsoft.rest.v2.http.HttpRequest; +import com.microsoft.rest.v2.http.HttpResponse; +import rx.Single; + +import java.io.IOException; + +/** + * A PollStrategy type that uses the Location header value to check the status of a long running + * operation. + */ +public final class LocationPollStrategy extends PollStrategy { + private final String fullyQualifiedMethodName; + private String locationUrl; + private boolean done; + + /** + * The name of the header that indicates that a long running operation will use the Location + * strategy. + */ + public static final String HEADER_NAME = "Location"; + + private LocationPollStrategy(String fullyQualifiedMethodName, String locationUrl) { + this.fullyQualifiedMethodName = fullyQualifiedMethodName; + this.locationUrl = locationUrl; + } + + @Override + public HttpRequest createPollRequest() { + return new HttpRequest(fullyQualifiedMethodName, "GET", locationUrl); + } + + @Override + public void updateFrom(HttpResponse httpPollResponse) throws IOException { + final int httpStatusCode = httpPollResponse.statusCode(); + if (httpStatusCode == 202) { + locationUrl = httpPollResponse.headerValue(HEADER_NAME); + updateDelayInMillisecondsFrom(httpPollResponse); + } + else { + done = true; + } + } + + @Override + public Single updateFromAsync(HttpResponse httpPollResponse) { + Single result; + try { + updateFrom(httpPollResponse); + result = Single.just(httpPollResponse); + } catch (IOException e) { + result = Single.error(e); + } + return result; + } + + @Override + public boolean isDone() { + return done; + } + + /** + * Try to create a new LocationOperationPollStrategy object that will poll the provided location + * URL. If the provided HttpResponse doesn't have a Location header or the header is empty, + * then null will be returned. + * @param fullyQualifiedMethodName The fully qualified name of the method that initiated the + * long running operation. + * @param httpResponse The HTTP response that the required header values for this pollStrategy + * will be read from. + */ + static LocationPollStrategy tryToCreate(String fullyQualifiedMethodName, HttpResponse httpResponse) { + final String locationUrl = httpResponse.headerValue(HEADER_NAME); + return locationUrl != null && !locationUrl.isEmpty() + ? new LocationPollStrategy(fullyQualifiedMethodName, locationUrl) + : null; + } +} \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationResource.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationResource.java new file mode 100644 index 0000000000000..fe4c9b4dcfedf --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationResource.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.v2; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The OperationResource class is a POJO representation of the Azure-AsyncOperation Operation + * Resource format (see https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/Addendum.md#operation-resource-format + * for more information). + */ +public class OperationResource { + @JsonProperty(value = "properties") + private Properties properties; + + /** + * Get the inner properties object. + * @return The inner properties object. + */ + public Properties properties() { + return properties; + } + + /** + * Set the properties of this OperationResource. + * @param properties The properties of this OperationResource. + */ + public void setProperties(Properties properties) { + this.properties = properties; + } + + /** + * Inner properties class. + */ + public static class Properties { + @JsonProperty(value = "provisioningState") + private String provisioningState; + + /** + * Get the provisioning state of the resource. + * @return The provisioning state of the resource. + */ + String provisioningState() { + return provisioningState; + } + + /** + * Set the provisioning state of this OperationResource. + * @param provisioningState The provisioning state of this OperationResource. + */ + public void setProvisioningState(String provisioningState) { + this.provisioningState = provisioningState; + } + } +} \ No newline at end of file diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java index 7de039366f5a6..8e1a1970bd394 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java @@ -6,18 +6,71 @@ package com.microsoft.azure.v2; +import com.microsoft.rest.protocol.SerializerAdapter; +import com.microsoft.rest.v2.http.HttpRequest; +import com.microsoft.rest.v2.http.HttpResponse; +import rx.Single; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + /** - * The status of a long running operation. This generally is created from the result of polling for - * whether a long running operation is done or not. - * @param + * The current state of polling for the result of a long running operation. */ -public final class OperationStatus { - private final boolean isDone; - private final T result; +class OperationStatus { + private PollStrategy pollStrategy; + private T result; - private OperationStatus(boolean isDone, T result) { - this.isDone = isDone; - this.result = result; + /** + * Create a new OperationStatus from the provided HTTP response. + * @param originalHttpRequest The HttpRequest that initiated the long running operation. + * @param originalHttpResponse The HttpResponse from the request that initiated the long running + * operation. + * @param serializer The serializer used to deserialize the response body. + */ + OperationStatus(HttpRequest originalHttpRequest, HttpResponse originalHttpResponse, SerializerAdapter serializer) { + final int httpStatusCode = originalHttpResponse.statusCode(); + + if (httpStatusCode != 200) { + final String fullyQualifiedMethodName = originalHttpRequest.callerMethod(); + final String originalHttpRequestMethod = originalHttpRequest.httpMethod(); + final String originalHttpRequestUrl = originalHttpRequest.url(); + + if (originalHttpRequestMethod.equalsIgnoreCase("PUT") || originalHttpRequestMethod.equalsIgnoreCase("PATCH")) { + if (httpStatusCode == 201) { + pollStrategy = AzureAsyncOperationPollStrategy.tryToCreate(fullyQualifiedMethodName, originalHttpResponse, originalHttpRequestUrl, serializer); + } else if (httpStatusCode == 202) { + pollStrategy = AzureAsyncOperationPollStrategy.tryToCreate(fullyQualifiedMethodName, originalHttpResponse, originalHttpRequestUrl, serializer); + if (pollStrategy == null) { + pollStrategy = LocationPollStrategy.tryToCreate(fullyQualifiedMethodName, originalHttpResponse); + } + } + } else /* if (originalRequestHttpMethod.equalsIgnoreCase("DELETE") || originalRequestHttpMethod.equalsIgnoreCase("POST") */ { + if (httpStatusCode == 202) { + pollStrategy = AzureAsyncOperationPollStrategy.tryToCreate(fullyQualifiedMethodName, originalHttpResponse, originalHttpRequestUrl, serializer); + if (pollStrategy == null) { + pollStrategy = LocationPollStrategy.tryToCreate(fullyQualifiedMethodName, originalHttpResponse); + } + } + } + } + } + + /** + * Update the properties of this OperationStatus from the provided response. + * @param httpResponse The HttpResponse from the most recent request. + */ + void updateFrom(HttpResponse httpResponse) throws IOException { + pollStrategy.updateFrom(httpResponse); + } + + /** + * Update the properties of this OperationStatus from the provided HTTP poll response. + * @param httpPollResponse The response of the most recent poll request. + * @return A Single that can be used to chain off of this operation. + */ + Single updateFromAsync(HttpResponse httpPollResponse) { + return pollStrategy.updateFromAsync(httpPollResponse); } /** @@ -25,35 +78,62 @@ private OperationStatus(boolean isDone, T result) { * @return Whether or not the long running operation is done. */ public boolean isDone() { - return isDone; + return pollStrategy == null || pollStrategy.isDone(); } /** - * If the long running operation is done, get the result of the operation. If the operation is - * not done, then return null. - * @return The result of the operation, or null if the operation isn't done yet. + * Create a HttpRequest that will get the next polling status update for the long running + * operation. + * @return A HttpRequest that will get the next polling status update for the long running + * operation. */ - public T result() { + HttpRequest createPollRequest() { + return pollStrategy.createPollRequest(); + } + + /** + * If this OperationStatus has a retryAfterSeconds value, delay (and block) the current thread for + * the number of seconds that are in the retryAfterSeconds value. If this OperationStatus doesn't + * have a retryAfterSeconds value, then just return. + */ + void delay() throws InterruptedException { + final long delayInMilliseconds = pollStrategy.delayInMilliseconds(); + if (delayInMilliseconds > 0) { + Thread.sleep(delayInMilliseconds); + } + } + + /** + * If this OperationStatus has a retryAfterSeconds value, return an Single that is delayed by the + * number of seconds that are in the retryAfterSeconds value. If this OperationStatus doesn't have + * a retryAfterSeconds value, then return an Single with no delay. + * @return A Single with delay if this OperationStatus has a retryAfterSeconds value. + */ + Single delayAsync() { + Single result = Single.just(null); + + final long delayInMilliseconds = pollStrategy.delayInMilliseconds(); + if (delayInMilliseconds > 0) { + result = result.delay(delayInMilliseconds, TimeUnit.MILLISECONDS); + } + return result; } /** - * Get an OperationStatus that represents a long running operation that is still in progress. - * @param The type of result that the long running operation will return. - * @return An OperationStatus that represents a long running operation that is still in - * progress. + * If the long running operation is done, get the result of the operation. If the operation is + * not done, then return null. + * @return The result of the operation, or null if the operation isn't done yet. */ - public static OperationStatus inProgress() { - return new OperationStatus<>(false, null); + public T result() { + return result; } /** - * Get an OperationStatus that represents a long running operation that has completed. - * @param result The result of the long running operation. - * @param The type of result that the long running operation will return. - * @return An OperationStatus that represents a long running operation that has completed. + * Set the result of this OperationStatus. + * @param result The result to assign to this OperationStatus. */ - public static OperationStatus completed(T result) { - return new OperationStatus<>(true, result); + void setResult(T result) { + this.result = result; } } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java new file mode 100644 index 0000000000000..1cf2923871bf2 --- /dev/null +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.v2; + +import com.microsoft.rest.v2.http.HttpRequest; +import com.microsoft.rest.v2.http.HttpResponse; +import rx.Single; + +import java.io.IOException; + +/** + * An abstract class for the different strategies that an OperationStatus can use when checking the + * status of a long running operation. + */ +abstract class PollStrategy { + private long delayInMilliseconds; + + /** + * Get the number of milliseconds to delay before sending the next poll request. + * @return The number of milliseconds to delay. + */ + final long delayInMilliseconds() { + return delayInMilliseconds; + } + + /** + * Set the delay in milliseconds to 0. + */ + final void clearDelayInMilliseconds() { + this.delayInMilliseconds = 0; + } + + /** + * Update the delay in milliseconds from the provided HTTP poll response. + * @param httpPollResponse The HTTP poll response to update the delay in milliseconds from. + */ + final void updateDelayInMillisecondsFrom(HttpResponse httpPollResponse) { + final String retryAfterSecondsString = httpPollResponse.headerValue("Retry-After"); + if (retryAfterSecondsString != null && !retryAfterSecondsString.isEmpty()) { + try { + final long responseDelayInMilliseconds = Long.valueOf(retryAfterSecondsString) * 1000; + delayInMilliseconds = Math.max(responseDelayInMilliseconds, AzureProxy.defaultDelayInMilliseconds()); + } + catch (NumberFormatException ignored) { + } + } + } + + /** + * Create a new HTTP poll request. + * @return A new HTTP poll request. + */ + abstract HttpRequest createPollRequest(); + + /** + * Update the status of this PollStrategy from the provided HTTP poll response. + * @param httpPollResponse The response of the most recent poll request. + */ + abstract void updateFrom(HttpResponse httpPollResponse) throws IOException; + + /** + * Update the status of this PollStrategy from the provided HTTP poll response. + * @param httpPollResponse The response of the most recent poll request. + * @return A Completable that can be used to chain off of this operation. + */ + abstract Single updateFromAsync(HttpResponse httpPollResponse); + + /** + * Get whether or not this PollStrategy's long running operation is done. + * @return Whether or not this PollStrategy's long running operation is done. + */ + abstract boolean isDone(); +} \ No newline at end of file diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java index 9f98d44f5cdec..38554f102b347 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java @@ -1,7 +1,6 @@ package com.microsoft.azure.v2; import com.microsoft.azure.v2.http.MockAzureHttpClient; -import com.microsoft.azure.v2.http.MockAzureHttpResponse; import com.microsoft.rest.protocol.SerializerAdapter; import com.microsoft.rest.serializer.JacksonAdapter; import com.microsoft.rest.v2.InvalidReturnTypeException; @@ -45,6 +44,14 @@ private interface MockResourceService { @ExpectedResponses({200}) MockResource createWithLocationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsRemaining); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation") + @ExpectedResponses({200}) + MockResource createWithAzureAsyncOperation(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName); + + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation&PollsRemaining={pollsRemaining}") + @ExpectedResponses({200}) + MockResource createWithAzureAsyncOperationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsRemaining); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}") @ExpectedResponses({200}) Single createAsync(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName); @@ -57,6 +64,14 @@ private interface MockResourceService { @ExpectedResponses({200}) Single createAsyncWithLocationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation") + @ExpectedResponses({200}) + Single createAsyncWithAzureAsyncOperation(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName); + + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation&PollsRemaining={pollsRemaining}") + @ExpectedResponses({200}) + Single createAsyncWithAzureAsyncOperationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Location&PollsRemaining={pollsRemaining}") @ExpectedResponses({200}) Observable beginCreateAsyncWithBadReturnType(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); @@ -65,6 +80,10 @@ private interface MockResourceService { @ExpectedResponses({200}) Observable> beginCreateAsyncWithLocationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); + @PUT("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}?PollType=Azure-AsyncOperation&PollsRemaining={pollsRemaining}") + @ExpectedResponses({200}) + Observable> beginCreateAsyncWithAzureAsyncOperationAndPolls(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName, @PathParam("pollsRemaining") int pollsUntilResource); + @DELETE("subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/mockprovider/mockresources/{mockResourceName}") @ExpectedResponses({200}) void delete(@PathParam("subscriptionId") String subscriptionId, @PathParam("resourceGroupName") String resourceGroupName, @PathParam("mockResourceName") String mockResourceName); @@ -170,6 +189,36 @@ public void createWithLocationAndPolls() { assertEquals(2, httpClient.pollRequests()); } + @Test + public void createWithAzureAsyncOperation() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final MockResource resource = createMockService(MockResourceService.class, httpClient) + .createWithAzureAsyncOperation("1", "mine", "c"); + assertNotNull(resource); + assertEquals("c", resource.name); + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(1, httpClient.pollRequests()); + } + + @Test + public void createWithAzureAsyncOperationAndPolls() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final MockResource resource = createMockService(MockResourceService.class, httpClient) + .createWithAzureAsyncOperationAndPolls("1", "mine", "c", 2); + assertNotNull(resource); + assertEquals("c", resource.name); + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(2, httpClient.pollRequests()); + } + @Test public void createAsync() { final MockAzureHttpClient httpClient = new MockAzureHttpClient(); @@ -218,6 +267,38 @@ public void createAsyncWithLocationAndPolls() { assertEquals(3, httpClient.pollRequests()); } + @Test + public void createAsyncWithAzureAsyncOperation() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final MockResource resource = createMockService(MockResourceService.class, httpClient) + .createAsyncWithAzureAsyncOperation("1", "mine", "c") + .toBlocking().value(); + assertNotNull(resource); + assertEquals("c", resource.name); + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(1, httpClient.pollRequests()); + } + + @Test + public void createAsyncWithAzureAsyncOperationAndPolls() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final MockResource resource = createMockService(MockResourceService.class, httpClient) + .createAsyncWithAzureAsyncOperationAndPolls("1", "mine", "c", 3) + .toBlocking().value(); + assertNotNull(resource); + assertEquals("c", resource.name); + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(3, httpClient.pollRequests()); + } + @Test public void beginCreateAsyncWithBadReturnType() { final MockAzureHttpClient httpClient = new MockAzureHttpClient(); @@ -269,6 +350,37 @@ public void call(OperationStatus operationStatus) { assertEquals(3, httpClient.pollRequests()); } + @Test + public void beginCreateAsyncWithAzureAsyncOperationAndPolls() { + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + + final AtomicInteger inProgressCount = new AtomicInteger(); + final Value resource = new Value<>(); + + createMockService(MockResourceService.class, httpClient) + .beginCreateAsyncWithAzureAsyncOperationAndPolls("1", "mine", "c", 3) + .subscribe(new Action1>() { + @Override + public void call(OperationStatus operationStatus) { + if (!operationStatus.isDone()) { + inProgressCount.incrementAndGet(); + } + else { + resource.set(operationStatus.result()); + } + } + }); + + assertEquals(3, inProgressCount.get()); + assertNotNull(resource.get()); + assertEquals("c", resource.get().name); + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(3, httpClient.pollRequests()); + } + @Test public void beginCreateAsyncWithLocationAndPollsWhenPollsUntilResourceIs0() { final MockAzureHttpClient httpClient = new MockAzureHttpClient(); @@ -447,130 +559,6 @@ private static T createMockService(Class serviceClass, MockAzureHttpClien return AzureProxy.create(serviceClass, httpClient, serializer); } - @Test - public void getPollUrlWithNoLocationHeaderAndNullCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200); - assertNull(AzureProxy.getPollUrl(response, null)); - } - - @Test - public void getPollUrlWithNullLocationHeaderAndNullCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", null); - assertNull(AzureProxy.getPollUrl(response, null)); - } - - @Test - public void getPollUrlWithEmptyLocationHeaderAndNullCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", ""); - assertNull(AzureProxy.getPollUrl(response, null)); - } - - @Test - public void getPollUrlWithNonEmptyLocationHeaderAndNullCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", "spam"); - assertEquals("spam", AzureProxy.getPollUrl(response, null)); - } - - @Test - public void getPollUrlWithNoLocationHeaderAndNonEmptyCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200); - assertEquals("peanut butter", AzureProxy.getPollUrl(response, "peanut butter")); - } - - @Test - public void getPollUrlWithNullLocationHeaderAndNonEmptyCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", null); - assertEquals("peanut butter", AzureProxy.getPollUrl(response, "peanut butter")); - } - - @Test - public void getPollUrlWithEmptyLocationHeaderAndNonEmptyCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", ""); - assertEquals("peanut butter", AzureProxy.getPollUrl(response, "peanut butter")); - } - - @Test - public void getPollUrlWithNonEmptyLocationHeaderAndNonEmptyCurrentPollUrl() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Location", "spam"); - assertEquals("spam", AzureProxy.getPollUrl(response, "peanut butter")); - } - - @Test - public void getRetryAfterSecondsWithNoHeaderAndNullCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200); - assertNull(AzureProxy.getRetryAfterSeconds(response, null)); - } - - @Test - public void getRetryAfterSecondsWithNullHeaderAndNullCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", null); - assertNull(AzureProxy.getRetryAfterSeconds(response, null)); - } - - @Test - public void getRetryAfterSecondsWithEmptyHeaderAndNullCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", ""); - assertNull(AzureProxy.getRetryAfterSeconds(response, null)); - } - - @Test - public void getRetryAfterSecondsWithNonIntegerHeaderAndNullCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", "spam"); - assertNull(AzureProxy.getRetryAfterSeconds(response, null)); - } - - @Test - public void getRetryAfterSecondsWithIntegerHeaderAndNullCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", "123"); - assertEquals(123, AzureProxy.getRetryAfterSeconds(response, null).longValue()); - } - - - - @Test - public void getRetryAfterSecondsWithNoHeaderAndCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200); - assertEquals(5, AzureProxy.getRetryAfterSeconds(response, 5L).longValue()); - } - - @Test - public void getRetryAfterSecondsWithNullHeaderAndCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", null); - assertEquals(5, AzureProxy.getRetryAfterSeconds(response, 5L).longValue()); - } - - @Test - public void getRetryAfterSecondsWithEmptyHeaderAndCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", ""); - assertEquals(5, AzureProxy.getRetryAfterSeconds(response, 5L).longValue()); - } - - @Test - public void getRetryAfterSecondsWithNonIntegerHeaderAndCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", "spam"); - assertEquals(5, AzureProxy.getRetryAfterSeconds(response, 5L).longValue()); - } - - @Test - public void getRetryAfterSecondsWithIntegerHeaderAndCurrentRetryAfterSeconds() { - final MockAzureHttpResponse response = new MockAzureHttpResponse(200) - .withHeader("Retry-After", "123"); - assertEquals(123, AzureProxy.getRetryAfterSeconds(response, 5L).longValue()); - } - private static void assertContains(String value, String expectedSubstring) { assertTrue("Expected \"" + value + "\" to contain \"" + expectedSubstring + "\".", value.contains(expectedSubstring)); } diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/http/MockAzureHttpClient.java b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/http/MockAzureHttpClient.java index e300c4966027b..055fdd6f2eaaf 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/http/MockAzureHttpClient.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/http/MockAzureHttpClient.java @@ -7,8 +7,11 @@ package com.microsoft.azure.v2.http; import com.google.common.io.CharStreams; +import com.microsoft.azure.v2.AzureAsyncOperationPollStrategy; import com.microsoft.azure.v2.HttpBinJSON; +import com.microsoft.azure.v2.LocationPollStrategy; import com.microsoft.azure.v2.MockResource; +import com.microsoft.azure.v2.OperationResource; import com.microsoft.rest.v2.http.HttpClient; import com.microsoft.rest.v2.http.HttpHeader; import com.microsoft.rest.v2.http.HttpHeaders; @@ -27,6 +30,8 @@ * This HttpClient attempts to mimic the behavior of http://httpbin.org without ever making a network call. */ public class MockAzureHttpClient extends HttpClient { + private int pollsRemaining; + private int getRequests; private int createRequests; private int deleteRequests; @@ -50,7 +55,7 @@ public int pollRequests() { @Override public Single sendRequestInternalAsync(HttpRequest request) { - HttpResponse response = null; + MockAzureHttpResponse response = null; try { final URI requestUrl = new URI(request.url()); @@ -113,59 +118,75 @@ else if (requestPathLower.contains("/mockprovider/mockoperations/")) { final String pollType = requestQueryMap.get("PollType"); - final String pollsRemainingString = requestQueryMap.get("PollsRemaining"); - int pollsRemaining; - try { - pollsRemaining = Integer.valueOf(pollsRemainingString); - } - catch (Exception ignored) { - pollsRemaining = 1; - } - - String returnValueString = requestQueryMap.get("ReturnValue"); - if (returnValueString == null) { - returnValueString = "false"; - } - - if (pollsRemaining <= 1) { - if ("true".equalsIgnoreCase(returnValueString)) { - final MockResource resource = new MockResource(); - resource.name = "c"; - response = new MockAzureHttpResponse(200, resource); + if (pollType.equalsIgnoreCase(AzureAsyncOperationPollStrategy.HEADER_NAME)) { + final OperationResource.Properties properties = new OperationResource.Properties(); + if (pollsRemaining <= 1) { + properties.setProvisioningState(AzureAsyncOperationPollStrategy.SUCCEEDED); } else { - response = new MockAzureHttpResponse(200); + --pollsRemaining; + properties.setProvisioningState(AzureAsyncOperationPollStrategy.IN_PROGRESS); } + final OperationResource operationResource = new OperationResource(); + operationResource.setProperties(properties); + response = new MockAzureHttpResponse(200, operationResource); } - else { - --pollsRemaining; - final String locationUrl = "https://mock.azure.com/subscriptions/1/resourceGroups/mine/providers/mockprovider/mockoperations/1?ReturnValue=" + returnValueString + "&PollType=" + pollType + "&PollsRemaining=" + pollsRemaining; - response = new MockAzureHttpResponse(202) - .withHeader("Location", locationUrl); + else if (pollType.equalsIgnoreCase(LocationPollStrategy.HEADER_NAME)) { + if (pollsRemaining <= 1) { + final MockResource mockResource = new MockResource(); + mockResource.name = "c"; + response = new MockAzureHttpResponse(200, mockResource); + } + else { + --pollsRemaining; + response = new MockAzureHttpResponse(202) + .withHeader(LocationPollStrategy.HEADER_NAME, request.url()); + } } } } else if (request.httpMethod().equalsIgnoreCase("PUT")) { ++createRequests; - final Map requestQueryMap = queryToMap(requestUrl.getQuery()); + final Map requestQueryMap = queryToMap(requestUrl.getQuery()); final String pollType = requestQueryMap.get("PollType"); - String pollsRemaining = requestQueryMap.get("PollsRemaining"); + String pollsRemainingString = requestQueryMap.get("PollsRemaining"); - if (pollType == null || "0".equals(pollsRemaining)) { + if (pollType == null || "0".equals(pollsRemainingString)) { final MockResource resource = new MockResource(); resource.name = "c"; response = new MockAzureHttpResponse(200, resource); - } - else if (pollType.equals("Location")) { - if (pollsRemaining == null) { - pollsRemaining = "1"; + } else { + + if (pollsRemainingString == null) { + pollsRemaining = 1; + } + else { + pollsRemaining = Integer.valueOf(pollsRemainingString); } - final String locationUrl = "https://mock.azure.com/subscriptions/1/resourceGroups/mine/providers/mockprovider/mockoperations/1?ReturnValue=true&PollType=Location&PollsRemaining=" + pollsRemaining; - response = new MockAzureHttpResponse(202) - .withHeader("Location", locationUrl); + final String initialResponseStatusCodeString = requestQueryMap.get("InitialResponseStatusCode"); + int initialResponseStatusCode; + if (initialResponseStatusCodeString != null) { + initialResponseStatusCode = Integer.valueOf(initialResponseStatusCodeString); + } + else if (pollType.equalsIgnoreCase(LocationPollStrategy.HEADER_NAME)) { + initialResponseStatusCode = 202; + } + else { + initialResponseStatusCode = 201; + } + + response = new MockAzureHttpResponse(initialResponseStatusCode); + + final String pollUrl = "https://mock.azure.com/subscriptions/1/resourceGroups/mine/providers/mockprovider/mockoperations/1"; + if (pollType.contains(AzureAsyncOperationPollStrategy.HEADER_NAME)) { + response.withHeader(AzureAsyncOperationPollStrategy.HEADER_NAME, pollUrl + "?PollType=" + AzureAsyncOperationPollStrategy.HEADER_NAME); + } + if (pollType.contains(LocationPollStrategy.HEADER_NAME)) { + response.withHeader(LocationPollStrategy.HEADER_NAME, pollUrl + "?PollType=" + LocationPollStrategy.HEADER_NAME); + } } } else if (request.httpMethod().equalsIgnoreCase("DELETE")) { @@ -174,19 +195,40 @@ else if (request.httpMethod().equalsIgnoreCase("DELETE")) { final Map requestQueryMap = queryToMap(requestUrl.getQuery()); final String pollType = requestQueryMap.get("PollType"); - String pollsRemaining = requestQueryMap.get("PollsRemaining"); + String pollsRemainingString = requestQueryMap.get("PollsRemaining"); - if (pollType == null || "0".equals(pollsRemaining)) { + if (pollType == null || "0".equals(pollsRemainingString)) { response = new MockAzureHttpResponse(200); } - else if (pollType.equals("Location")) { - if (pollsRemaining == null) { - pollsRemaining = "1"; + else if (pollType.equals(LocationPollStrategy.HEADER_NAME)) { + if (pollsRemainingString == null) { + pollsRemaining = 1; + } + else { + pollsRemaining = Integer.valueOf(pollsRemainingString); } - final String locationUrl = "https://mock.azure.com/subscriptions/1/resourceGroups/mine/providers/mockprovider/mockoperations/1?ReturnValue=false&PollType=Location&PollsRemaining=" + pollsRemaining; - response = new MockAzureHttpResponse(202) - .withHeader("Location", locationUrl); + final String initialResponseStatusCodeString = requestQueryMap.get("InitialResponseStatusCode"); + int initialResponseStatusCode; + if (initialResponseStatusCodeString != null) { + initialResponseStatusCode = Integer.valueOf(initialResponseStatusCodeString); + } + else if (pollType.equalsIgnoreCase(LocationPollStrategy.HEADER_NAME)) { + initialResponseStatusCode = 202; + } + else { + initialResponseStatusCode = 201; + } + + response = new MockAzureHttpResponse(initialResponseStatusCode); + + final String pollUrl = "https://mock.azure.com/subscriptions/1/resourceGroups/mine/providers/mockprovider/mockoperations/1"; + if (pollType.contains(AzureAsyncOperationPollStrategy.HEADER_NAME)) { + response.withHeader(AzureAsyncOperationPollStrategy.HEADER_NAME, pollUrl + "?PollType=" + AzureAsyncOperationPollStrategy.HEADER_NAME); + } + if (pollType.contains(LocationPollStrategy.HEADER_NAME)) { + response.withHeader(LocationPollStrategy.HEADER_NAME, pollUrl + "?PollType=" + LocationPollStrategy.HEADER_NAME); + } } } } @@ -194,7 +236,7 @@ else if (pollType.equals("Location")) { catch (Exception ignored) { } - return Single.just(response); + return Single.just(response); } private static Map queryToMap(String url) { diff --git a/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java b/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java index 37e5d4dd330e0..c91999e573809 100644 --- a/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java +++ b/client-runtime/src/main/java/com/microsoft/rest/v2/RestProxy.java @@ -62,6 +62,14 @@ private SwaggerMethodParser methodParser(Method method) { return interfaceParser.methodParser(method); } + /** + * Get the SerializerAdapter used by this RestProxy. + * @return The SerializerAdapter used by this RestProxy. + */ + protected SerializerAdapter serializer() { + return serializer; + } + /** * Send the provided request and block until the response is received. * @param request The HTTP request to send. @@ -90,11 +98,11 @@ public Object invoke(Object proxy, final Method method, Object[] args) throws IO Object result; if (methodParser.isAsync()) { final Single asyncResponse = sendHttpRequestAsync(request); - result = handleAsyncHttpResponse(asyncResponse, methodParser); + result = handleAsyncHttpResponse(request, asyncResponse, methodParser); } else { final HttpResponse response = sendHttpRequest(request); - result = handleSyncHttpResponse(response, methodParser); + result = handleSyncHttpResponse(request, response, methodParser); } return result; @@ -134,12 +142,12 @@ private HttpRequest createHttpRequest(SwaggerMethodParser methodParser, Object[] return request; } - protected Object handleSyncHttpResponse(HttpResponse httpResponse, SwaggerMethodParser methodParser) throws IOException, InterruptedException { + protected Object handleSyncHttpResponse(HttpRequest httpRequest, HttpResponse httpResponse, SwaggerMethodParser methodParser) throws IOException, InterruptedException { final Type returnType = methodParser.returnType(); - return handleSyncHttpResponse(httpResponse, methodParser, returnType); + return handleSyncHttpResponse(httpRequest, httpResponse, methodParser, returnType); } - protected Object handleSyncHttpResponse(HttpResponse httpResponse, SwaggerMethodParser methodParser, Type returnType) throws IOException { + protected Object handleSyncHttpResponse(HttpRequest httpRequest, HttpResponse httpResponse, SwaggerMethodParser methodParser, Type returnType) throws IOException { Object result; final TypeToken returnTypeToken = TypeToken.of(returnType); @@ -182,7 +190,7 @@ protected Object handleSyncHttpResponse(HttpResponse httpResponse, SwaggerMethod return result; } - protected Object handleAsyncHttpResponse(Single asyncHttpResponse, final SwaggerMethodParser methodParser) { + protected Object handleAsyncHttpResponse(HttpRequest httpRequest, Single asyncHttpResponse, final SwaggerMethodParser methodParser) { Object result; final Type returnType = methodParser.returnType(); From 6d8d5fdf6c0c4963e9dc1d6d3da56a317e3046c5 Mon Sep 17 00:00:00 2001 From: Dan Schulte Date: Tue, 19 Sep 2017 15:36:07 -0700 Subject: [PATCH 2/3] Make AzureProxy public --- .../src/main/java/com/microsoft/azure/v2/AzureProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java index 1dbc94ff1da62..80a01f21653c3 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java @@ -30,7 +30,7 @@ * This class can be used to create an Azure specific proxy implementation for a provided Swagger * generated interface. */ -final class AzureProxy extends RestProxy { +public final class AzureProxy extends RestProxy { private static long defaultDelayInMilliseconds = 30 * 1000; /** From 0538c84cfb695e5c87bf752fcf6a608b1ac65040 Mon Sep 17 00:00:00 2001 From: Dan Schulte Date: Thu, 21 Sep 2017 09:15:27 -0700 Subject: [PATCH 3/3] Fix code review comments --- .../v2/AzureAsyncOperationPollStrategy.java | 52 ++++++++----------- .../com/microsoft/azure/v2/AzureProxy.java | 14 ++--- .../azure/v2/LocationPollStrategy.java | 2 + .../microsoft/azure/v2/OperationStatus.java | 3 +- .../com/microsoft/azure/v2/PollStrategy.java | 7 ++- .../microsoft/azure/v2/AzureProxyTests.java | 44 ++++++++++++++++ 6 files changed, 79 insertions(+), 43 deletions(-) diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java index 2a6691563860e..961f5ae88372e 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureAsyncOperationPollStrategy.java @@ -23,7 +23,8 @@ public final class AzureAsyncOperationPollStrategy extends PollStrategy { private final String operationResourceUrl; private final String originalResourceUrl; private final SerializerAdapter serializer; - private Boolean pollingSucceeded; + private boolean pollingCompleted; + private boolean pollingSucceeded; private boolean gotResourceResponse; /** @@ -48,10 +49,14 @@ public final class AzureAsyncOperationPollStrategy extends PollStrategy { * @param fullyQualifiedMethodName The fully qualified name of the method that initiated the * long running operation. * @param operationResourceUrl The URL of the operation resource this pollStrategy will poll. + * @param originalResourceUrl The URL of the resource that the long running operation is + * operating on. * @param serializer The serializer that will deserialize the operation resource and the * final operation result. */ private AzureAsyncOperationPollStrategy(String fullyQualifiedMethodName, String operationResourceUrl, String originalResourceUrl, SerializerAdapter serializer) { + super(AzureProxy.defaultDelayInMilliseconds()); + this.fullyQualifiedMethodName = fullyQualifiedMethodName; this.operationResourceUrl = operationResourceUrl; this.originalResourceUrl = originalResourceUrl; @@ -61,7 +66,7 @@ private AzureAsyncOperationPollStrategy(String fullyQualifiedMethodName, String @Override public HttpRequest createPollRequest() { String pollUrl = null; - if (pollingSucceeded == null) { + if (!pollingCompleted) { pollUrl = operationResourceUrl; } else if (pollingSucceeded) { @@ -72,31 +77,31 @@ else if (pollingSucceeded) { @Override public void updateFrom(HttpResponse httpPollResponse) throws IOException { - updateDelayInMillisecondsFrom(httpPollResponse); - - if (pollingSucceeded == null) { - final String bodyString = httpPollResponse.bodyAsString(); - updateFromResponseBodyString(bodyString); - } - else if (pollingSucceeded) { - gotResourceResponse = true; - } + updateFromAsync(httpPollResponse).toBlocking().value(); } @Override public Single updateFromAsync(final HttpResponse httpPollResponse) { - Single result; - - if (pollingSucceeded == null) { - updateDelayInMillisecondsFrom(httpPollResponse); + updateDelayInMillisecondsFrom(httpPollResponse); + Single result; + if (!pollingCompleted) { result = httpPollResponse.bodyAsStringAsync() .flatMap(new Func1>() { @Override public Single call(String bodyString) { - Single result = Single.just(httpPollResponse); + Single result; try { - updateFromResponseBodyString(bodyString); + final OperationResource operationResource = serializer.deserialize(bodyString, OperationResource.class); + if (operationResource != null) { + final String provisioningState = provisioningState(operationResource); + pollingCompleted = !IN_PROGRESS.equalsIgnoreCase(provisioningState); + if (pollingCompleted) { + pollingSucceeded = SUCCEEDED.equalsIgnoreCase(provisioningState); + clearDelayInMilliseconds(); + } + } + result = Single.just(httpPollResponse); } catch (IOException e) { result = Single.error(e); } @@ -115,17 +120,6 @@ public Single call(String bodyString) { return result; } - private void updateFromResponseBodyString(String httpResponseBodyString) throws IOException { - final OperationResource operationResource = serializer.deserialize(httpResponseBodyString, OperationResource.class); - if (operationResource != null) { - final String provisioningState = provisioningState(operationResource); - if (!IN_PROGRESS.equalsIgnoreCase(provisioningState)) { - pollingSucceeded = SUCCEEDED.equalsIgnoreCase(provisioningState); - clearDelayInMilliseconds(); - } - } - } - private static String provisioningState(OperationResource operationResource) { String provisioningState = null; @@ -139,7 +133,7 @@ private static String provisioningState(OperationResource operationResource) { @Override public boolean isDone() { - return pollingSucceeded != null && (!pollingSucceeded || gotResourceResponse); + return pollingCompleted && (!pollingSucceeded || gotResourceResponse); } /** diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java index 80a01f21653c3..2c61dfa8d35d1 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/AzureProxy.java @@ -45,9 +45,7 @@ private AzureProxy(HttpClient httpClient, SerializerAdapter serializer, Swagg } /** - * Get the millisecond delay that will occur by default between long running operation polls. - * @return The millisecond delay that will occur by default between long running operation - * polls. + * @return The millisecond delay that will occur by default between long running operation polls. */ public static long defaultDelayInMilliseconds() { return AzureProxy.defaultDelayInMilliseconds; @@ -72,7 +70,7 @@ public static void setDefaultDelayInMilliseconds(long defaultDelayInMilliseconds * @return A proxy implementation of the provided Swagger interface. */ @SuppressWarnings("unchecked") - public static A create(Class swaggerInterface, final HttpClient httpClient, SerializerAdapter serializer) { + public static A create(Class swaggerInterface, HttpClient httpClient, SerializerAdapter serializer) { /* FIXME: get URL from @AzureHost */ final String baseUrl = null; @@ -174,13 +172,7 @@ public Observable> call(HttpResponse httpResponse) { public Boolean call(OperationStatus operationStatus) { return operationStatus.isDone(); } - }) - .concatWith(Observable.defer(new Func0>>() { - @Override - public Observable> call() { - return Observable.just(operationStatus); - } - })); + }); } return result; } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java index 5bad92b2dcb3e..45151385b3101 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/LocationPollStrategy.java @@ -28,6 +28,8 @@ public final class LocationPollStrategy extends PollStrategy { public static final String HEADER_NAME = "Location"; private LocationPollStrategy(String fullyQualifiedMethodName, String locationUrl) { + super(AzureProxy.defaultDelayInMilliseconds()); + this.fullyQualifiedMethodName = fullyQualifiedMethodName; this.locationUrl = locationUrl; } diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java index 8e1a1970bd394..0d4851f816c52 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/OperationStatus.java @@ -16,8 +16,9 @@ /** * The current state of polling for the result of a long running operation. + * @param The type of value that will be returned from the long running operation. */ -class OperationStatus { +public class OperationStatus { private PollStrategy pollStrategy; private T result; diff --git a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java index 1cf2923871bf2..a5a88cdcc245a 100644 --- a/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java +++ b/azure-client-runtime/src/main/java/com/microsoft/azure/v2/PollStrategy.java @@ -19,6 +19,10 @@ abstract class PollStrategy { private long delayInMilliseconds; + PollStrategy(long delayInMilliseconds) { + this.delayInMilliseconds = delayInMilliseconds; + } + /** * Get the number of milliseconds to delay before sending the next poll request. * @return The number of milliseconds to delay. @@ -42,8 +46,7 @@ final void updateDelayInMillisecondsFrom(HttpResponse httpPollResponse) { final String retryAfterSecondsString = httpPollResponse.headerValue("Retry-After"); if (retryAfterSecondsString != null && !retryAfterSecondsString.isEmpty()) { try { - final long responseDelayInMilliseconds = Long.valueOf(retryAfterSecondsString) * 1000; - delayInMilliseconds = Math.max(responseDelayInMilliseconds, AzureProxy.defaultDelayInMilliseconds()); + delayInMilliseconds = Long.valueOf(retryAfterSecondsString) * 1000; } catch (NumberFormatException ignored) { } diff --git a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java index 38554f102b347..658e39e065134 100644 --- a/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java +++ b/azure-client-runtime/src/test/java/com/microsoft/azure/v2/AzureProxyTests.java @@ -10,6 +10,10 @@ import com.microsoft.rest.v2.annotations.Host; import com.microsoft.rest.v2.annotations.PUT; import com.microsoft.rest.v2.annotations.PathParam; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import rx.Completable; import rx.Observable; @@ -21,6 +25,18 @@ import static org.junit.Assert.*; public class AzureProxyTests { + private long delayInMillisecondsBackup; + + @Before + public void beforeTest() { + delayInMillisecondsBackup = AzureProxy.defaultDelayInMilliseconds(); + AzureProxy.setDefaultDelayInMilliseconds(0); + } + + @After + public void afterTest() { + AzureProxy.setDefaultDelayInMilliseconds(delayInMillisecondsBackup); + } @Host("https://mock.azure.com") private interface MockResourceService { @@ -299,6 +315,34 @@ public void createAsyncWithAzureAsyncOperationAndPolls() { assertEquals(3, httpClient.pollRequests()); } + @Test + public void createAsyncWithAzureAsyncOperationAndPollsWithDelay() throws InterruptedException { + final long delayInMilliseconds = 100; + AzureProxy.setDefaultDelayInMilliseconds(delayInMilliseconds); + + final MockAzureHttpClient httpClient = new MockAzureHttpClient(); + final int pollsUntilResource = 3; + createMockService(MockResourceService.class, httpClient) + .createAsyncWithAzureAsyncOperationAndPolls("1", "mine", "c", pollsUntilResource) + .subscribe(); + + Thread.sleep((long)(delayInMilliseconds * 0.75)); + + for (int i = 0; i < pollsUntilResource; ++i) { + assertEquals(0, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(i, httpClient.pollRequests()); + + Thread.sleep(delayInMilliseconds); + } + + assertEquals(1, httpClient.getRequests()); + assertEquals(1, httpClient.createRequests()); + assertEquals(0, httpClient.deleteRequests()); + assertEquals(pollsUntilResource, httpClient.pollRequests()); + } + @Test public void beginCreateAsyncWithBadReturnType() { final MockAzureHttpClient httpClient = new MockAzureHttpClient();