/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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.linecorp.armeria.client.circuitbreaker; import static java.util.Objects.requireNonNull; import java.time.Duration; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; /** * A non-blocking implementation of circuit breaker pattern. */ final class NonBlockingCircuitBreaker implements CircuitBreaker { private static final Logger logger = LoggerFactory.getLogger(NonBlockingCircuitBreaker.class); private static final AtomicLong seqNo = new AtomicLong(0); private final String name; private final CircuitBreakerConfig config; private final AtomicReference<State> state; private final Ticker ticker; /** * Creates a new {@link NonBlockingCircuitBreaker} with the specified {@link Ticker} and * {@link CircuitBreakerConfig}. */ NonBlockingCircuitBreaker(Ticker ticker, CircuitBreakerConfig config) { this.ticker = requireNonNull(ticker, "ticker"); this.config = requireNonNull(config, "config"); name = config.name().orElseGet(() -> "circuit-breaker-" + seqNo.getAndIncrement()); state = new AtomicReference<>(newClosedState()); logStateTransition(CircuitState.CLOSED, null); notifyStateChanged(CircuitState.CLOSED); } @Override public String name() { return name; } @Override public void onSuccess() { final State currentState = state.get(); if (currentState.isClosed()) { // fires success event final Optional<EventCount> updatedCount = currentState.counter().onSuccess(); // notifies the count if it has been updated updatedCount.ifPresent(this::notifyCountUpdated); } else if (currentState.isHalfOpen()) { // changes to CLOSED if at least one request succeeds during HALF_OPEN if (state.compareAndSet(currentState, newClosedState())) { logStateTransition(CircuitState.CLOSED, null); notifyStateChanged(CircuitState.CLOSED); } } } @Override public void onFailure(Throwable cause) { try { if (cause != null && !config.exceptionFilter().shouldDealWith(cause)) { return; } } catch (Exception e) { logger.error("an exception has occured when calling an ExceptionFilter", e); } onFailure(); } @Override public void onFailure() { final State currentState = state.get(); if (currentState.isClosed()) { // fires failure event final Optional<EventCount> updatedCount = currentState.counter().onFailure(); // checks the count if it has been updated updatedCount.ifPresent(count -> { // changes to OPEN if failure rate exceeds the threshold if (checkIfExceedingFailureThreshold(count) && state.compareAndSet(currentState, newOpenState())) { logStateTransition(CircuitState.OPEN, count); notifyStateChanged(CircuitState.OPEN); } else { notifyCountUpdated(count); } }); } else if (currentState.isHalfOpen()) { // returns to OPEN if a request fails during HALF_OPEN if (state.compareAndSet(currentState, newOpenState())) { logStateTransition(CircuitState.OPEN, null); notifyStateChanged(CircuitState.OPEN); } } } private boolean checkIfExceedingFailureThreshold(EventCount count) { return 0 < count.total() && config.minimumRequestThreshold() <= count.total() && config.failureRateThreshold() < count.failureRate(); } @Override public boolean canRequest() { final State currentState = state.get(); if (currentState.isClosed()) { // all requests are allowed during CLOSED return true; } else if (currentState.isHalfOpen() || currentState.isOpen()) { if (currentState.checkTimeout() && state.compareAndSet(currentState, newHalfOpenState())) { // changes to HALF_OPEN if OPEN state has timed out logStateTransition(CircuitState.HALF_OPEN, null); notifyStateChanged(CircuitState.HALF_OPEN); return true; } // all other requests are refused notifyRequestRejected(); return false; } return true; } private State newOpenState() { return new State(CircuitState.OPEN, config.circuitOpenWindow(), NoOpCounter.INSTANCE); } private State newHalfOpenState() { return new State(CircuitState.HALF_OPEN, config.trialRequestInterval(), NoOpCounter.INSTANCE); } private State newClosedState() { return new State( CircuitState.CLOSED, Duration.ZERO, new SlidingWindowCounter(ticker, config.counterSlidingWindow(), config.counterUpdateInterval())); } private void logStateTransition(CircuitState circuitState, @Nullable EventCount count) { if (logger.isInfoEnabled()) { final int capacity = name.length() + circuitState.name().length() + 32; final StringBuilder builder = new StringBuilder(capacity); builder.append("name:"); builder.append(name); builder.append(" state:"); builder.append(circuitState.name()); if (count != null) { builder.append(" fail:"); builder.append(count.failure()); builder.append(" total:"); builder.append(count.total()); } logger.info(builder.toString()); } } private void notifyStateChanged(CircuitState circuitState) { config.listeners().forEach(listener -> { try { listener.onStateChanged(this, circuitState); } catch (Throwable t) { logger.warn("An error occured when notifying a state changed event", t); } try { listener.onEventCountUpdated(this, EventCount.ZERO); } catch (Throwable t) { logger.warn("An error occured when notifying an EventCount updated event", t); } }); } private void notifyCountUpdated(EventCount count) { config.listeners().forEach(listener -> { try { listener.onEventCountUpdated(this, count); } catch (Throwable t) { logger.warn("An error occured when notifying an EventCount updated event", t); } }); } private void notifyRequestRejected() { config.listeners().forEach(listener -> { try { listener.onRequestRejected(this); } catch (Throwable t) { logger.warn("An error occured when notifying a request rejected event", t); } }); } @VisibleForTesting State state() { return state.get(); } @VisibleForTesting CircuitBreakerConfig config() { return config; } /** * The internal state of the circuit breaker. */ final class State { private final CircuitState circuitState; private final EventCounter counter; private final long timedOutTimeNanos; /** * Creates a new instance. * * @param circuitState The circuit state * @param timeoutDuration The max duration of the state * @param counter The event counter to use during the state */ private State(CircuitState circuitState, Duration timeoutDuration, EventCounter counter) { this.circuitState = circuitState; this.counter = counter; if (timeoutDuration.isZero() || timeoutDuration.isNegative()) { timedOutTimeNanos = 0L; } else { timedOutTimeNanos = ticker.read() + timeoutDuration.toNanos(); } } private EventCounter counter() { return counter; } /** * Returns {@code true} if this state has timed out. */ private boolean checkTimeout() { return 0 < timedOutTimeNanos && timedOutTimeNanos <= ticker.read(); } boolean isOpen() { return circuitState == CircuitState.OPEN; } boolean isHalfOpen() { return circuitState == CircuitState.HALF_OPEN; } boolean isClosed() { return circuitState == CircuitState.CLOSED; } } private static class NoOpCounter implements EventCounter { private static final NoOpCounter INSTANCE = new NoOpCounter(); @Override public EventCount count() { return EventCount.ZERO; } @Override public Optional<EventCount> onSuccess() { return Optional.empty(); } @Override public Optional<EventCount> onFailure() { return Optional.empty(); } } }