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

com.proofpoint.http.client.balancing.HttpServiceBalancerImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2013 Proofpoint, 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.proofpoint.http.client.balancing;

import com.google.common.annotations.Beta;
import com.google.common.base.Ticker;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.proofpoint.http.client.balancing.HttpServiceBalancerStats.Status;
import com.proofpoint.stats.MaxGauge;
import com.proofpoint.units.Duration;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.weakref.jmx.Nested;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

public class HttpServiceBalancerImpl
        implements HttpServiceBalancer
{
    private static final InstanceState INSTANCE_STATE_WORST = new InstanceState(Liveness.DEAD, Integer.MAX_VALUE);
    private static final Duration ZERO_DURATION = new Duration(0, SECONDS);
    private final AtomicReference> httpUris = new AtomicReference<>(ImmutableMultiset.of());

    @GuardedBy("uriStates")
    private final Map uriStates = new HashMap<>();
    private final String description;
    private final HttpServiceBalancerStats httpServiceBalancerStats;
    private final int consecutiveFailures;
    private final BackoffPolicy backoffPolicy;
    private final Ticker ticker;
    private final MaxGauge concurrency = new MaxGauge();

    public HttpServiceBalancerImpl(String description, HttpServiceBalancerStats httpServiceBalancerStats, HttpServiceBalancerConfig config)
    {
        this(description, httpServiceBalancerStats, config, Ticker.systemTicker());
    }

    HttpServiceBalancerImpl(String description, HttpServiceBalancerStats httpServiceBalancerStats, HttpServiceBalancerConfig config, Ticker ticker)
    {
        this.description = requireNonNull(description, "description is null");
        this.httpServiceBalancerStats = requireNonNull(httpServiceBalancerStats, "httpServiceBalancerStats is null");
        consecutiveFailures = requireNonNull(config, "config is null").getConsecutiveFailures();
        backoffPolicy = new DecorrelatedJitteredBackoffPolicy(config.getMinBackoff(), config.getMaxBackoff());
        this.ticker = requireNonNull(ticker, "ticker is null");
    }

    @Override
    public HttpServiceAttempt createAttempt()
    {
        return new HttpServiceAttemptImpl(Set.of());
    }

    @Beta
    public void updateHttpUris(Collection newHttpUris)
    {
        httpUris.set(ImmutableMultiset.copyOf(newHttpUris));
    }

    private class HttpServiceAttemptImpl
            implements HttpServiceAttempt
    {
        private final Set attempted;
        private final URI uri;
        private final long startTick;
        private boolean inProgress = true;

        HttpServiceAttemptImpl(Set attempted)
        {
            Set attemptedCopy = attempted;
            Multiset httpUris = HttpServiceBalancerImpl.this.httpUris.get().stream()
                    .filter(uri -> !attemptedCopy.contains(uri))
                    .collect(Collectors.toCollection(HashMultiset::create));

            if (httpUris.isEmpty()) {
                httpUris = HttpServiceBalancerImpl.this.httpUris.get();
                attempted = Set.of();

                if (httpUris.isEmpty()) {
                    throw new ServiceUnavailableException(description);
                }
            }

            InstanceState bestState = INSTANCE_STATE_WORST;
            List leastUris = new ArrayList<>();
            synchronized (uriStates) {
                long now = ticker.read();
                for (;;) {
                    for (Entry uriEntry : httpUris.entrySet()) {
                        URI uri = uriEntry.getElement();
                        InstanceState uriState = uriStates.computeIfAbsent(uri, k -> new InstanceState(Liveness.ALIVE, 0));
                        if (uriState.weight != uriEntry.getCount()) {
                            uriState.weight = uriEntry.getCount();
                        }
                        if (uriState.liveness == Liveness.DEAD && uriState.deadUntil <= now) {
                            uriState.liveness = Liveness.PROBING;
                        }
                        int comparison = uriState.compareTo(bestState);
                        if (comparison <= 0) {
                            if (comparison < 0) {
                                bestState = uriState;
                                leastUris = new ArrayList<>();
                            }
                            for (int i = uriState.weight - (uriState.concurrency % uriState.weight); i > 0; i--) {
                                leastUris.add(uri);
                            }
                        }
                    }

                    if (bestState.liveness != Liveness.DEAD || attempted.isEmpty()) {
                        break;
                    }

                    httpUris = HttpServiceBalancerImpl.this.httpUris.get();
                    attempted = Set.of();
                }

                uri = leastUris.get(ThreadLocalRandom.current().nextInt(0, leastUris.size()));

                InstanceState uriState = uriStates.get(uri);
                if (uriState.liveness == Liveness.PROBING && uriState.concurrency == 0) {
                    httpServiceBalancerStats.probe(uri).add(1);
                }

                if (uriState.concurrency++ == concurrency.get()) {
                    concurrency.update(uriState.concurrency);
                }
            }

            this.attempted = Set.copyOf(attempted);
            startTick = ticker.read();
        }

        @Override
        public URI getUri()
        {
            return uri;
        }

        @Override
        public void markGood()
        {
            decrementConcurrency(false);
            httpServiceBalancerStats.requestTime(uri, Status.SUCCESS).add(ticker.read() - startTick, TimeUnit.NANOSECONDS);
        }

        @Override
        public void markBad(String failureCategory)
        {
            decrementConcurrency(true);
            httpServiceBalancerStats.requestTime(uri, Status.FAILURE).add(ticker.read() - startTick, TimeUnit.NANOSECONDS);
            httpServiceBalancerStats.failure(uri, failureCategory).add(1);
        }

        @Override
        public void markBad(String failureCategory, String handlerCategory)
        {
            decrementConcurrency(true);
            httpServiceBalancerStats.requestTime(uri, Status.FAILURE).add(ticker.read() - startTick, TimeUnit.NANOSECONDS);
            httpServiceBalancerStats.failure(uri, failureCategory, handlerCategory).add(1);
        }

        private void decrementConcurrency(boolean isFailure)
        {
            checkState(inProgress, "is in progress");
            inProgress = false;
            synchronized (uriStates) {
                InstanceState uriState = uriStates.get(uri);

                uriState.liveness.mark(isFailure, uriState, this, HttpServiceBalancerImpl.this);
                int oldConcurrency = uriState.concurrency;
                if (oldConcurrency > 0) {
                    --uriState.concurrency;
                }

                if (oldConcurrency == 1 && !isFailure && uriState.liveness == Liveness.ALIVE) {
                    uriStates.remove(uri);
                    if (uriStates.isEmpty()) {
                        concurrency.update(0);
                        return;
                    }
                }
                if (concurrency.get() == oldConcurrency) {
                    for (InstanceState instanceState : uriStates.values()) {
                        if (oldConcurrency == instanceState.concurrency) {
                            return;
                        }
                    }
                    concurrency.update(oldConcurrency - 1);
                }
            }
        }

        @Override
        public HttpServiceAttempt next()
        {
            checkState(!inProgress, "is not still in progress");
            Set newAttempted = ImmutableSet.builder()
                    .add(uri)
                    .addAll(attempted)
                    .build();
            return new HttpServiceAttemptImpl(newAttempted);
        }
    }

    @Nested
    public MaxGauge getConcurrency()
    {
        return concurrency;
    }

    @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "Object does not implement Comparable")
    private static class InstanceState
    {
        Liveness liveness;
        int weight = 1;
        int concurrency;
        int numFailures = 0;
        BackoffPolicy backoffPolicy;
        Duration lastBackoff;
        long deadUntil;

        InstanceState(Liveness liveness, int concurrency)
        {
            this.liveness = liveness;
            this.concurrency = concurrency;
        }

        int compareTo(InstanceState that)
        {
            if (liveness == Liveness.DEAD || (liveness == Liveness.PROBING && concurrency > 0)) {
                if (that.liveness == Liveness.DEAD || (that.liveness == Liveness.PROBING && that.concurrency > 0)) {
                    return Integer.compare(concurrency / weight, that.concurrency / that.weight);
                }
                return 1;
            }
            if (that.liveness == Liveness.DEAD || (that.liveness == Liveness.PROBING && that.concurrency > 0)) {
                return -1;
            }
            return Integer.compare(concurrency / weight, that.concurrency / that.weight);
        }
    }

    private enum Liveness
    {
        ALIVE {
            @Override
            public void mark(boolean isFailure, InstanceState uriState, HttpServiceAttemptImpl attempt, HttpServiceBalancerImpl balancer)
            {
                if (isFailure) {
                    if (++uriState.numFailures >= balancer.consecutiveFailures) {
                        uriState.liveness = DEAD;
                        uriState.backoffPolicy = balancer.backoffPolicy;
                        uriState.lastBackoff = uriState.backoffPolicy.backoff(ZERO_DURATION);
                        uriState.deadUntil = balancer.ticker.read() + uriState.lastBackoff.roundTo(NANOSECONDS);
                        balancer.httpServiceBalancerStats.removal(attempt.uri).add(uriState.lastBackoff);
                    }
                }
                else {
                    uriState.numFailures = 0;
                }
            }
        },

        DEAD {
            @Override
            public void mark(boolean isFailure, InstanceState uriState, HttpServiceAttemptImpl attempt, HttpServiceBalancerImpl balancer)
            {
                if (!isFailure) {
                    uriState.liveness = ALIVE;
                    uriState.numFailures = 0;
                    uriState.backoffPolicy = null;
                    uriState.lastBackoff = null;
                    balancer.httpServiceBalancerStats.revival(attempt.uri).add(1);
                }
            }
        },

        PROBING {
            @Override
            public void mark(boolean isFailure, InstanceState uriState, HttpServiceAttemptImpl attempt, HttpServiceBalancerImpl balancer)
            {
                if (isFailure) {
                    uriState.liveness = DEAD;
                    uriState.backoffPolicy = uriState.backoffPolicy.nextAttempt();
                    uriState.lastBackoff = uriState.backoffPolicy.backoff(uriState.lastBackoff);
                    uriState.deadUntil = balancer.ticker.read() + uriState.lastBackoff.roundTo(NANOSECONDS);
                    balancer.httpServiceBalancerStats.removal(attempt.uri).add(uriState.lastBackoff);
                }
                else {
                    uriState.liveness = ALIVE;
                    uriState.numFailures = 0;
                    uriState.backoffPolicy = null;
                    uriState.lastBackoff = null;
                    balancer.httpServiceBalancerStats.revival(attempt.uri).add(1);
                }
            }
        };

        public abstract void mark(boolean isFailure, InstanceState uriState, HttpServiceAttemptImpl attempt, HttpServiceBalancerImpl balancer);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy