Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial design for blocking operation #4134

Merged
merged 12 commits into from
Jun 28, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package com.azure.core.util.polling;

import com.azure.core.util.ExpandableStringEnum;

import java.time.Duration;
import java.util.Map;
import java.util.Objects;
Expand All @@ -15,6 +17,9 @@
*<p><strong>Code Sample Creating PollResponse Object</strong></p>
* {@codesnippet com.azure.core.util.polling.pollresponse.status.value}
*
* <p><strong>Code Sample Creating PollResponse Object with custom status</strong></p>
* {@codesnippet com.azure.core.util.polling.pollresponse.custom.status.value}
*
* @param <T> Type of poll response value.
*
* @see OperationStatus
Expand All @@ -26,38 +31,39 @@ public final class PollResponse<T> {
private final T value;
private final Duration retryAfter;
private final Map<Object, Object> properties;
private final String otherStatus;

/**
* An enum to represent all possible states that a long-running operation may find itself in.
* The poll operation is considered complete when the status is one of {@code SUCCESSFULLY_COMPLETED}, {@code USER_CANCELLED} or {@code FAILED}.
*/
public enum OperationStatus {
public static final class OperationStatus extends ExpandableStringEnum<OperationStatus> {
/** Represents that polling has not yet started for this long-running operation.*/
NOT_STARTED,
public static final OperationStatus NOT_STARTED = fromString("NOT_STARTED");

/** Represents that this long-running operation is in progress and not yet complete.*/
IN_PROGRESS,
public static final OperationStatus IN_PROGRESS = fromString("IN_PROGRESS");

/** Represent that this long-running operation is completed successfully.*/
SUCCESSFULLY_COMPLETED,
public static final OperationStatus SUCCESSFULLY_COMPLETED = fromString("SUCCESSFULLY_COMPLETED");

/**
* Represents that this long-running operation has failed to successfully complete, however this is still
* considered as complete long-running operation, meaning that the {@link Poller} instance will report that it is complete.
*/
FAILED,
public static final OperationStatus FAILED = fromString("FAILED");

/** Represents that this long-running operation is cancelled by user, however this is still
* considered as complete long-running operation.*/
USER_CANCELLED,
public static final OperationStatus USER_CANCELLED = fromString("USER_CANCELLED");

/**
* When long-running operation status could not be represented by any status in {@link OperationStatus}, this status represents
* a custom status Azure service could be in. This custom status is not considered as complete long-running operation.
* It must have valid value for {@code otherStatus} as {@link String}.
* Creates or finds a {@link OperationStatus} from its string representation.
* @param name a name to look for
* @return the corresponding {@link OperationStatus}
*/
OTHER
public static OperationStatus fromString(String name) {
return fromString(name, OperationStatus.class);
}
}

/**
Expand All @@ -73,28 +79,12 @@ public enum OperationStatus {
* @param properties A map of properties provided by the service that will be made available into the next poll operation.
* @throws NullPointerException If {@code status} is {@code null}.
*/
public PollResponse(OperationStatus status, T value, Duration retryAfter, Map<Object, Object> properties) {
this(status, null, value, retryAfter, properties);
}

/*
* Creates a new {@link PollResponse} with status, value and retryAfter.
*
* @param status Mandatory operation status as defined in {@link OperationStatus}.
* @param otherStatus string representation of custom status. It must be not null and non empty. The status will be defaulted to {@link OperationStatus#OTHER}
* @param value The value as a result of poll operation. This can be any custom user-defined object. Null is also valid.
* @param retryAfter Represents the delay the service has requested until the next polling operation is performed.
* A {@code null}, zero or negative value will be taken to mean that the {@link Poller} should determine on its own when the next poll operation is to occur.
* @param properties A map of properties provided by the service that will be made available into the next poll operation.
* @throws NullPointerException If {@code status} is {@code null}.
*/
private PollResponse(OperationStatus status, String otherStatus, T value, Duration retryAfter, Map<Object, Object> properties) {
public PollResponse(OperationStatus status, T value, Duration retryAfter, Map<Object, Object> properties) {
Objects.requireNonNull(status, "The status input parameter cannot be null.");
this.status = status;
this.value = value;
this.retryAfter = retryAfter;
this.properties = properties;
this.otherStatus = otherStatus;
}

/**
Expand Down Expand Up @@ -127,49 +117,6 @@ public PollResponse(OperationStatus status, T value) {
this(status, value, null);
}

/**
* Creates a new {@link PollResponse} with status and value.
*
*<p><strong>Code Sample Creating PollResponse Object</strong></p>
* {@codesnippet com.azure.core.util.polling.pollresponse.custom.status.retryAfter}
*
* @param otherStatus string representation of custom status. It must be not null and non empty. The status will be defaulted to {@link OperationStatus#OTHER}
* @param value The value as a result of poll operation. This can be any custom user-defined object. Null is also valid.
* @param retryAfter Represents the delay the service has requested until the next polling operation is performed.
* A {@code null}, zero or negative value will be taken to mean that the {@link Poller} should determine on its own when the next poll operation is to occur.
* @throws NullPointerException If {@code status} is {@code null}.
* @throws IllegalArgumentException if otherStatus is null or empty when status is {@link OperationStatus#OTHER}.
*/
public PollResponse(String otherStatus, T value, Duration retryAfter) {
this(OperationStatus.OTHER, otherStatus, value, retryAfter, null);
if (Objects.isNull(otherStatus) || otherStatus.trim().length() == 0) {
throw new IllegalArgumentException("The otherStatus can not be empty or null.");
}
}

/**
* Creates a new {@link PollResponse} with custom status and value.
*
*<p><strong>Code Sample Creating PollResponse Object</strong></p>
* {@codesnippet com.azure.core.util.polling.pollresponse.custom.status}
*
* @param otherStatus string representation of custom status. It must be not null and non empty. The status will be defaulted to {@link OperationStatus#OTHER}
* @param value The value as a result of poll operation. This can be any custom user-defined object. Null is also valid.
* @throws NullPointerException If {@code status} is {@code null}.
* @throws IllegalArgumentException if otherStatus is null or empty when status is {@link OperationStatus#OTHER}.
*/
public PollResponse(String otherStatus, T value) {
this(otherStatus, value, null);
}

/**
* Used to retrieve value for custom status string when status is {@link OperationStatus#OTHER}.
* @return custom other status string when status is {@link OperationStatus#OTHER}.
*/
public String getOtherStatus() {
return this.otherStatus;
}

/**
* Represents the status of the long-running operation at the time the last polling operation finished successfully.
* @return A {@link OperationStatus} representing the result of the poll operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

import com.azure.core.util.polling.PollResponse.OperationStatus;

/**
* This class offers API that simplifies the task of executing long-running operations against Azure service.
* The {@link Poller} consist of poll operation, cancel operation if supported by Azure service and polling interval.
Expand All @@ -38,7 +39,7 @@
* <p>When auto-polling is disabled, the {@link Poller} will not update its status or other information, unless manual polling is triggered by calling {@link Poller#poll()} function.
*
* <p>The {@link Poller} will stop polling when the long-running operation is complete or it is disabled. The polling is considered complete
* based on status defined in {@link com.azure.core.util.polling.PollResponse.OperationStatus}.
* based on status defined in {@link OperationStatus}.
*
* <p><strong>Code Samples</strong></p>
*
Expand All @@ -53,7 +54,7 @@
*
* @param <T> Type of poll response value
* @see PollResponse
* @see com.azure.core.util.polling.PollResponse.OperationStatus
* @see OperationStatus
*/
public class Poller<T> {

Expand Down Expand Up @@ -108,23 +109,23 @@ public class Poller<T> {
*
* @param pollInterval Not-null and greater than zero poll interval.
* @param pollOperation The polling operation to be called by the {@link Poller} instance. This is a callback into the client library,
* which must never return {@code null}, and which must always have a non-null {@link com.azure.core.util.polling.PollResponse.OperationStatus}.
* which must never return {@code null}, and which must always have a non-null {@link OperationStatus}.
*{@link Mono} returned from poll operation should never return {@link Mono#error(Throwable)}.If any unexpected scenario happens in poll operation,
* it should be handled by client library and return a valid {@link PollResponse}. However if poll operation returns {@link Mono#error(Throwable)},
* the {@link Poller} will disregard that and continue to poll.
* @throws NullPointerException If {@code pollInterval} or {@code pollOperation} are {@code null}.
* @throws IllegalArgumentException if {@code pollInterval} is less than or equal to zero.
* @throws IllegalArgumentException if {@code pollInterval} is less than or equal to zero and if {@code pollInterval} or {@code pollOperation} are {@code null}
*/
public Poller(Duration pollInterval, Function<PollResponse<T>, Mono<PollResponse<T>>> pollOperation) {
Objects.requireNonNull(pollInterval, "The poll interval input parameter cannot be null.");
if (pollInterval.toNanos() <= 0) {
throw new IllegalArgumentException("Negative or zero value for poll interval not allowed.");
if (pollInterval == null || pollInterval.toNanos() <= 0) {
throw new IllegalArgumentException("Null, negative or zero value for poll interval is not allowed.");
}
if (pollOperation == null) {
throw new IllegalArgumentException("Null value for poll operation is not allowed.");
}
Objects.requireNonNull(pollOperation, "The poll operation input parameter cannot be null.");

this.pollInterval = pollInterval;
this.pollOperation = pollOperation;
this.pollResponse = new PollResponse<>(PollResponse.OperationStatus.NOT_STARTED, null);
this.pollResponse = new PollResponse<>(OperationStatus.NOT_STARTED, null);

this.fluxHandle = asyncPollRequestWithDelay()
.flux()
Expand All @@ -144,14 +145,13 @@ public Poller(Duration pollInterval, Function<PollResponse<T>, Mono<PollResponse
*
* @param pollInterval Not-null and greater than zero poll interval.
* @param pollOperation The polling operation to be called by the {@link Poller} instance. This is a callback into the client library,
* which must never return {@code null}, and which must always have a non-null {@link com.azure.core.util.polling.PollResponse.OperationStatus}.
* which must never return {@code null}, and which must always have a non-null {@link OperationStatus}.
*{@link Mono} returned from poll operation should never return {@link Mono#error(Throwable)}.If any unexpected scenario happens in poll operation,
* it should handle it and return a valid {@link PollResponse}. However if poll operation returns {@link Mono#error(Throwable)},
* the {@link Poller} will disregard that and continue to poll.
* @param cancelOperation cancel operation if cancellation is supported by the service. It can be {@code null} which will indicate to the {@link Poller}
* that cancel operation is not supported by Azure service.
* @throws NullPointerException If {@code pollInterval} or {@code pollOperation} are {@code null}.
* @throws IllegalArgumentException if {@code pollInterval} is less than or equal to zero.
* @throws IllegalArgumentException if {@code pollInterval} is less than or equal to zero and if {@code pollInterval} or {@code pollOperation} are {@code null}
*/
public Poller(Duration pollInterval, Function<PollResponse<T>, Mono<PollResponse<T>>> pollOperation, Consumer<Poller> cancelOperation) {
this(pollInterval, pollOperation);
Expand All @@ -162,7 +162,7 @@ public Poller(Duration pollInterval, Function<PollResponse<T>, Mono<PollResponse
* Attempts to cancel the long-running operation that this {@link Poller} represents. This is possible only if the service supports it,
* otherwise an {@code UnsupportedOperationException} will be thrown.
* <p>
* It will call cancelOperation if status is {@link com.azure.core.util.polling.PollResponse.OperationStatus#IN_PROGRESS} otherwise it does nothing.
* It will call cancelOperation if status is {@link OperationStatus#IN_PROGRESS} otherwise it does nothing.
*
* @throws UnsupportedOperationException when cancel operation is not provided.
*/
Expand All @@ -173,7 +173,7 @@ public void cancelOperation() throws UnsupportedOperationException {

// We can not cancel an operation if it was never started
// It only make sense to call cancel operation if current status IN_PROGRESS.
if (this.pollResponse != null && this.pollResponse.getStatus() != PollResponse.OperationStatus.IN_PROGRESS) {
if (this.pollResponse != null && this.pollResponse.getStatus() != OperationStatus.IN_PROGRESS) {
return;
}

Expand Down Expand Up @@ -214,10 +214,10 @@ public Mono<PollResponse<T>> poll() {
}

/**
* Blocks execution and wait for polling to complete. The polling is considered complete based on status defined in {@link com.azure.core.util.polling.PollResponse.OperationStatus}.
* Blocks execution and wait for polling to complete. The polling is considered complete based on status defined in {@link OperationStatus}.
* <p>It will enable auto-polling if it was disable by user.
*
* @return returns final {@link PollResponse} when polling is complete as defined in {@link com.azure.core.util.polling.PollResponse.OperationStatus}.
* @return returns final {@link PollResponse} when polling is complete as defined in {@link OperationStatus}.
*/
public PollResponse<T> block() {
if (!isAutoPollingEnabled()) {
Expand All @@ -226,6 +226,57 @@ public PollResponse<T> block() {
return this.fluxHandle.blockLast();
}

/**
* Blocks indefinitely until given {@link OperationStatus} is received.
* @param statusToBlockFor The desired {@link OperationStatus} to block for and it can be any valid {@link OperationStatus} value.
* @return {@link PollResponse} for matching desired status.
* @throws IllegalArgumentException If {@code statusToBlockFor} is {@code null}.
*/
public PollResponse<T> blockUntil(OperationStatus statusToBlockFor) {
return blockUntil(statusToBlockFor, null);
}

/**
* Blocks until given {@link OperationStatus} is received or a timeout expires if provided. A {@code null} {@code timeout} will cause to block indefinitely for desired status.
* @param statusToBlockFor The desired {@link OperationStatus} to block for and it can be any valid {@link OperationStatus} value.
* @param timeout The time after which it will stop blocking. A {@code null} value will cause to block indefinitely. Zero or negative are not valid values.
* @return {@link PollResponse} for matching desired status to block for.
* @throws IllegalArgumentException if {@code timeout} is zero or negative and if {@code statusToBlockFor} is {@code null}.
*/
public PollResponse<T> blockUntil(OperationStatus statusToBlockFor, Duration timeout) {
hemanttanwar marked this conversation as resolved.
Show resolved Hide resolved
if (statusToBlockFor == null) {
throw new IllegalArgumentException("Null value for status is not allowed.");
}
if (timeout != null && timeout.toNanos() <= 0) {
throw new IllegalArgumentException("Negative or zero value for timeout is not allowed.");
}
if (!isAutoPollingEnabled()) {
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved
setAutoPollingEnabled(true);
}
if (timeout != null) {
return this.fluxHandle.filter(tPollResponse -> matchStatus(tPollResponse, statusToBlockFor)).blockFirst(timeout);
} else {
return this.fluxHandle.filter(tPollResponse -> matchStatus(tPollResponse, statusToBlockFor)).blockFirst();
}
}

/*
* Indicate that the @{link PollResponse} matches with the status to block for.
* @param currentPollResponse The poll response which we have received from the flux.
* @param statusToBlockFor The {@link OperationStatus} to block and it can be any valid {@link OperationStatus} value.
* @return True if the {@link PollResponse} return status matches the status to block for.
*/
private boolean matchStatus(PollResponse<T> currentPollResponse, OperationStatus statusToBlockFor) {
// perform validation
if (currentPollResponse == null || statusToBlockFor == null) {
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
if (statusToBlockFor == currentPollResponse.getStatus()) {
return true;
}
return false;
}

/*
* This function will apply delay and call poll operation function async.
* We expect Mono from pollOperation should never return Mono.error() . If any unexpected
Expand Down Expand Up @@ -295,9 +346,9 @@ public final void setAutoPollingEnabled(boolean autoPollingEnabled) {
* @return true if operation is done/complete.
*/
private boolean hasCompleted() {
return pollResponse != null && (pollResponse.getStatus() == PollResponse.OperationStatus.SUCCESSFULLY_COMPLETED
|| pollResponse.getStatus() == PollResponse.OperationStatus.FAILED
|| pollResponse.getStatus() == PollResponse.OperationStatus.USER_CANCELLED);
return pollResponse != null && (pollResponse.getStatus() == OperationStatus.SUCCESSFULLY_COMPLETED
|| pollResponse.getStatus() == OperationStatus.FAILED
|| pollResponse.getStatus() == OperationStatus.USER_CANCELLED);
}

/*
Expand All @@ -320,7 +371,7 @@ public boolean isAutoPollingEnabled() {
*
* @return current status or {@code null} if no status is available.
*/
public PollResponse.OperationStatus getStatus() {
public OperationStatus getStatus() {
return this.pollResponse != null ? this.pollResponse.getStatus() : null;
}
}
Loading