All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.java.view.ViewRetryHandler Maven / Gradle / Ivy

/*
 * Copyright (c) 2016 Couchbase, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.couchbase.client.java.view;

import com.couchbase.client.core.CouchbaseException;
import com.couchbase.client.core.RequestCancelledException;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.view.ViewQueryResponse;
import rx.Observable;
import rx.functions.Func1;

import java.util.concurrent.TimeUnit;

/**
 * Generic View retry handler based on response code and value inspection.
 *
 * @author Michael Nitschinger
 * @since 2.0.2
 */
public class ViewRetryHandler {

    private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(ViewRetryHandler.class);

    private static final ShouldRetryViewRequestException SHOULD_RETRY = new ShouldRetryViewRequestException();

    static {
        SHOULD_RETRY.setStackTrace(new StackTraceElement[]{});
    }

    private ViewRetryHandler() {}

    /**
     * Takes a {@link ViewQueryResponse}, verifies their status based on fixed criteria and resubscribes if needed.
     *
     * If it needs to be retried, the resubscription will happen after 10 milliseconds to give the underlying code
     * some time to recover.
     *
     * @param input the original response.
     * @return the good response which can be parsed, or a failing observable.
     */
    public static Observable retryOnCondition(final Observable input) {
        return input
            .flatMap(new Func1>() {
                @Override
                public Observable call(final ViewQueryResponse response) {
                    return passThroughOrThrow(response);
                }
            })
            .retryWhen(new Func1, Observable>() {
                @Override
                public Observable call(Observable observable) {
                    return observable
                        .flatMap(new Func1>() {
                            @Override
                            public Observable call(Throwable throwable) {
                                if (throwable instanceof ShouldRetryViewRequestException
                                    || throwable instanceof RequestCancelledException) {
                                    return Observable.timer(10, TimeUnit.MILLISECONDS);
                                } else {
                                    return Observable.error(throwable);
                                }
                            }
                        });
                }
            })
            .last();
    }

    /**
     * Helper method which decides if the response is good to pass through or needs to be retried.
     *
     * @param response the response to look at.
     * @return the {@link ViewQueryResponse} if it can pass through or an error if it needs to be retried.
     */
    private static Observable passThroughOrThrow(final ViewQueryResponse response) {
        final int responseCode = response.responseCode();
        if (responseCode == 200) {
            return Observable.just(response);
        }

        return response
            .error()
            .map(new Func1() {
                @Override
                public ViewQueryResponse call(String error) {
                    if (shouldRetry(responseCode, error)) {
                        throw SHOULD_RETRY;
                    }
                    return response;
                }
            })
            .singleOrDefault(response);
    }

    /**
     * Analyses status codes and checks if a retry needs to happen.
     *
     * Some status codes are ambiguous, so their contents are inspected further.
     *
     * @param status the status code.
     * @param content the error body from the response.
     * @return true if retry is needed, false otherwise.
     */
    private static boolean shouldRetry(final int status, final String content) {
        switch (status) {
            case 200:
                return false;
            case 404:
                return analyse404Response(content);
            case 500:
                return analyse500Response(content);
            case 300:
            case 301:
            case 302:
            case 303:
            case 307:
            case 401:
            case 408:
            case 409:
            case 412:
            case 416:
            case 417:
            case 501:
            case 502:
            case 503:
            case 504:
                return true;
            default:
                LOGGER.info("Received a View HTTP response code ({}) I did not expect, not retrying.", status);
                return false;
        }
    }

    /**
     * Analyses the content of a 404 response to see if it is legible for retry.
     *
     * If the content contains ""reason":"missing"", it is a clear indication that the responding node
     * is unprovisioned and therefore it should be retried. All other cases indicate a provisioned node,
     * but the design document/view is not found, which should not be retried.
     *
     * @param content the parsed error content.
     * @return true if it needs to be retried, false otherwise.
     */
    private static boolean analyse404Response(final String content) {
        if (content.contains("\"reason\":\"missing\"")) {
            return true;
        }

        LOGGER.debug("Design document not found, error is {}", content);
        return false;
    }

    /**
     * Analyses the content of a 500 response to see if it is legible for retry.
     *
     * @param content the parsed error content.
     * @return true if it needs to be retried, false otherwise.
     */
    private static boolean analyse500Response(final String content) {
        if (content.contains("error") && content.contains("{not_found, missing_named_view}")) {
            LOGGER.debug("Design document not found, error is {}", content);
            return false;
        }
        if (content.contains("error") && content.contains("\"badarg\"")) {
            LOGGER.debug("Malformed view query");
            return false;
        }
        return true;
    }

    /**
     * Exception type indicating a view needs to be retried.
     */
    private static class ShouldRetryViewRequestException extends CouchbaseException { }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy