001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.concurrent;
018
019import java.util.concurrent.atomic.AtomicLong;
020
021/**
022 * A simple implementation of the <a
023 * href="https://martinfowler.com/bliki/CircuitBreaker.html">Circuit Breaker</a> pattern
024 * that opens if the requested increment amount is greater than a given threshold.
025 *
026 * <p>
027 * It contains an internal counter that starts in zero, and each call increments the counter by a given amount.
028 * If the threshold is zero, the circuit breaker will be in a permanent <em>open</em> state.
029 * </p>
030 *
031 * <p>
032 * An example of use case could be a memory circuit breaker.
033 * </p>
034 *
035 * <pre>
036 * long threshold = 10L;
037 * ThresholdCircuitBreaker breaker = new ThresholdCircuitBreaker(10L);
038 * ...
039 * public void handleRequest(Request request) {
040 *     long memoryUsed = estimateMemoryUsage(request);
041 *     if (breaker.incrementAndCheckState(memoryUsed)) {
042 *         // actually handle this request
043 *     } else {
044 *         // do something else, e.g. send an error code
045 *     }
046 * }
047 * </pre>
048 *
049 * <p>
050 * #Thread safe#
051 * </p>
052 *
053 * @since 3.5
054 */
055public class ThresholdCircuitBreaker extends AbstractCircuitBreaker<Long> {
056
057    /**
058     * The initial value of the internal counter.
059     */
060    private static final long INITIAL_COUNT = 0L;
061
062    /**
063     * The threshold.
064     */
065    private final long threshold;
066
067    /**
068     * Controls the amount used.
069     */
070    private final AtomicLong used;
071
072    /**
073     * Creates a new instance of {@link ThresholdCircuitBreaker} and initializes the threshold.
074     *
075     * @param threshold The threshold.
076     */
077    public ThresholdCircuitBreaker(final long threshold) {
078        this.used = new AtomicLong(INITIAL_COUNT);
079        this.threshold = threshold;
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    @Override
086    public boolean checkState() {
087        return !isOpen();
088    }
089
090    /**
091     * {@inheritDoc}
092     *
093     * <p>
094     * Resets the internal counter back to its initial value (zero).
095     * </p>
096     */
097    @Override
098    public void close() {
099        super.close();
100        this.used.set(INITIAL_COUNT);
101    }
102
103    /**
104     * Gets the threshold.
105     *
106     * @return The threshold
107     */
108    public long getThreshold() {
109        return threshold;
110    }
111
112    /**
113     * {@inheritDoc}
114     *
115     * <p>
116     * If the threshold is zero, the circuit breaker will be in a permanent <em>open</em> state.
117     * </p>
118     * <p>
119     * The internal counter is a protective counter and only moves toward the threshold: negative
120     * increments are rejected, and an increment that would overflow {@link Long#MAX_VALUE} saturates
121     * the counter at {@link Long#MAX_VALUE} and opens the circuit breaker instead of silently wrapping
122     * negative (which would disable the trip condition).
123     * </p>
124     *
125     * @throws IllegalArgumentException Thrown if the increment is negative.
126     */
127    @Override
128    public boolean incrementAndCheckState(final Long increment) {
129        if (threshold == 0) {
130            open();
131        }
132        final long delta = increment.longValue();
133        if (delta < 0) {
134            throw new IllegalArgumentException("Increment must not be negative: " + delta);
135        }
136        final long used = this.used.accumulateAndGet(delta, (current, add) -> {
137            final long next = current + add;
138            // Both operands are non-negative, so overflow shows up as a decrease: saturate.
139            return next < current ? Long.MAX_VALUE : next;
140        });
141        if (used > threshold || used == Long.MAX_VALUE) {
142            open();
143        }
144        return checkState();
145    }
146
147}