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

com.azure.cosmos.implementation.directconnectivity.Uri Maven / Gradle / Ivy

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation.directconnectivity;

import com.azure.cosmos.implementation.directconnectivity.addressEnumerator.AddressEnumerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

public class Uri {
    private static final Logger logger = LoggerFactory.getLogger(Uri.class);

    private static long DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS = 60 * 1000;
    private final String uriAsString;
    private final URI uri;
    private final AtomicReference healthStatusTuple;

    private volatile Instant lastUnknownTimestamp;
    private volatile Instant lastUnhealthyPendingTimestamp;
    private volatile Instant lastTransitionToUnhealthyTimestamp;
    private volatile boolean isPrimary;
    public static final String ATTEMPTING = "Attempting";
    public static final String IGNORING = "Ignoring";
    private static final String PRIMARY = "P";
    private static final String SECONDARY = "S";

    public static Uri create(String uriAsString) {
        return new Uri(uriAsString);
    }

    public Uri(String uri) {
        this.uriAsString = uri;

        URI uriValue = null;
        try {
            uriValue = URI.create(uri);
        } catch (IllegalArgumentException e) {
            uriValue = null;
        }
        this.uri = uriValue;
        this.healthStatusTuple = new AtomicReference<>(
            new HealthStatusAndDiagnosticStringTuple(uriValue, HealthStatus.Unknown));
        this.lastUnknownTimestamp = Instant.now();
        this.lastUnhealthyPendingTimestamp = null;
        this.lastTransitionToUnhealthyTimestamp = null;
    }

    public URI getURI() {
        return this.uri;
    }

    public String getURIAsString() {
        return this.uriAsString;
    }

    public void setPrimary(boolean primary) {
        isPrimary = primary;
        if (primary) {
            this.healthStatusTuple.updateAndGet(previousStatusTuple ->
                new HealthStatusAndDiagnosticStringTuple(this.uri, previousStatusTuple.status, true));
        }
    }

    /***
     * Attention: This is only used for fault injection to easier detect whether the address is primary address.
     * @return
     */
    public boolean isPrimary() { return this.isPrimary; }

    /***
     * This method will be called if a connection can be established successfully to the backend.
     */
    public void setConnected() {
        this.setHealthStatus(HealthStatus.Connected);
    }

    /***
     * This method will be called if a request failed with 410 and will cause forceRefresh behavior.
     */
    public void setUnhealthy() {
        this.setHealthStatus(HealthStatus.Unhealthy);
    }

    /***
     * This method will be called if the same address being returned from gateway.
     *
     * Unknown will remain Unknown.
     * Connected will remain Connected.
     * UnhealthyPending will remain UnhealthyPending.
     * Unhealthy will change into UnhealthyPending.
     */
    public void setRefreshed() {
        if (this.healthStatusTuple.get().status == HealthStatus.Unhealthy) {
            this.setHealthStatus(HealthStatus.UnhealthyPending);
        }
    }

    /***
     * Please reference the /docs/replicaValidation/ReplicaClientSideStatus.png for valid status transition.
     *
     * @param status the health status.
     */
    public void setHealthStatus(HealthStatus status) {
        this.healthStatusTuple.updateAndGet(previousStatusTuple -> {

            HealthStatus newStatus = previousStatusTuple.status;
            switch (status) {
                case Unhealthy:
                    if (previousStatusTuple.status != HealthStatus.Unhealthy || this.lastTransitionToUnhealthyTimestamp == null) {
                        this.lastTransitionToUnhealthyTimestamp = Instant.now();
                    }
                    newStatus = status;
                    break;

                case UnhealthyPending:
                    if (previousStatusTuple.status == HealthStatus.Unhealthy || previousStatusTuple.status == HealthStatus.UnhealthyPending) {
                        this.lastUnhealthyPendingTimestamp = Instant.now();
                        newStatus = status;
                    }
                    break;
                case Connected:
                    if (previousStatusTuple.status != HealthStatus.Unhealthy
                        || (previousStatusTuple.status == HealthStatus.Unhealthy &&
                            Instant.now().compareTo(this.lastTransitionToUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0)) {
                        newStatus = status;
                    }
                    break;
                case Unknown:
                    // there is no reason we are going to reach here
                    throw new IllegalStateException("It is impossible to set to unknown status");
                default:
                    throw new IllegalStateException("Unsupported health status: " + status);
            }

            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Called setHealthStatus with status [{}]. Result: previousStatus [{}], newStatus [{}]",
                        status, previousStatusTuple, newStatus);
            }

            return new HealthStatusAndDiagnosticStringTuple(this.uri, newStatus, this.isPrimary);
        });
    }

    public HealthStatus getHealthStatus() {
        return this.healthStatusTuple.get().status;
    }

    /***
     * In {@link AddressEnumerator}, it could de-prioritize uri in unhealthyPending/unhealthy health status (depending on whether replica validation is enabled)
     * If the replica stuck in those statuses for too long, in order to avoid replica usage skew,
     * we are going to rolling them into healthy category, so it is status can be validated by requests again
     *
     * @return
     */
    public HealthStatus getEffectiveHealthStatus() {
        HealthStatus snapshot = this.healthStatusTuple.get().status;
        switch (snapshot) {
            case Connected:
            case Unhealthy:
                return snapshot;
            case Unknown:
                if (Instant.now()
                        .compareTo(this.lastUnknownTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0) {
                    return HealthStatus.Connected;
                }
                return snapshot;
            case UnhealthyPending:
                if (Instant.now()
                        .compareTo(this.lastUnhealthyPendingTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) > 0) {
                    return HealthStatus.Connected;
                }
                return snapshot;
            default:
                throw new IllegalStateException("Unknown status " + snapshot);
        }
    }

    public boolean shouldRefreshHealthStatus() {
        return this.healthStatusTuple.get().status == HealthStatus.Unhealthy
                && Instant.now().compareTo(this.lastTransitionToUnhealthyTimestamp.plusMillis(DEFAULT_NON_HEALTHY_RESET_TIME_IN_MILLISECONDS)) >= 0;
    }

    public String getHealthStatusDiagnosticString() {
        return this.healthStatusTuple.get().diagnosticString;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Uri uri1 = (Uri) o;
        return uriAsString.equals(uri1.uriAsString) && uri.equals(uri1.uri);
    }

    @Override
    public int hashCode() {
        return Objects.hash(uriAsString, uri);
    }

    @Override
    public String toString() {
        return this.uriAsString;
    }


    /***
     * 

* NOTE: Please DO NOT change the priority of the enums, * as it is used in {@link AddressEnumerator} for correct sorting purpose * * if you are going to change the priority of this, please reexamine the sorting logic as well. *

*/ public enum HealthStatus { Connected(100), Unknown(200), UnhealthyPending(300), Unhealthy(400); private int priority; HealthStatus(int priority) { this.priority = priority; } public int getPriority() { return this.priority; } } static class HealthStatusAndDiagnosticStringTuple { private String diagnosticString; private final HealthStatus status; public HealthStatusAndDiagnosticStringTuple(URI uri, HealthStatus status) { this(uri, status, false); } public HealthStatusAndDiagnosticStringTuple(URI uri, HealthStatus status, boolean isPrimary) { this.diagnosticString = uri.getPort() + ":" + (isPrimary ? Uri.PRIMARY : Uri.SECONDARY) + ":" + status; this.status = status; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy