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

com.coveo.spillway.Spillway Maven / Gradle / Ivy

There is a newer version: 3.0.0
Show newest version
/**
 * The MIT License
 * Copyright (c) 2016 Coveo
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.coveo.spillway;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.coveo.spillway.exception.SpillwayLimitExceededException;
import com.coveo.spillway.limit.Limit;
import com.coveo.spillway.limit.LimitBuilder;
import com.coveo.spillway.limit.LimitDefinition;
import com.coveo.spillway.limit.LimitKey;
import com.coveo.spillway.storage.LimitUsageStorage;
import com.coveo.spillway.storage.utils.AddAndGetRequest;
import com.coveo.spillway.trigger.LimitTrigger;

import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

/**
 * Contains methods to easily interact with the defined limits in the storage
 * and test if the incoming query should be throttled.
 * 

* Should always be built using the {@link SpillwayFactory}. * * @param The type of the context. String if not using a propertyExtractor * ({@link LimitBuilder#of(String, java.util.function.Function)}). * * @author Guillaume Simard * @author Emile Fugulin * @since 1.0.0 */ public class Spillway { private static final Logger logger = LoggerFactory.getLogger(Spillway.class); private final Clock clock; private final LimitUsageStorage storage; private final String resource; private final List> limits; @SafeVarargs public Spillway(Clock clock, LimitUsageStorage storage, String resourceName, Limit... limits) { this.clock = clock; this.storage = storage; this.resource = resourceName; this.limits = Collections.unmodifiableList(Arrays.asList(limits)); } /** * Behave like {@link #call(Object, int)} with {@code cost} of one. * * @see #call(Object, int) * * @param context Either the name of the limit OR the object on which the propertyExtractor ({@link LimitBuilder#of(String, java.util.function.Function)}) * will be applied if it was specified * @throws SpillwayLimitExceededException If one the enforced limit is exceeded */ public void call(T context) throws SpillwayLimitExceededException { call(context, 1); } /** * Verify if the query should be throttled using the specified cost. * * @param context Either the name of the limit OR the object on which the propertyExtractor ({@link LimitBuilder#of(String, java.util.function.Function)}) * will be applied if it was specified * @param cost The cost of the query * @throws SpillwayLimitExceededException If one the enforced limit is exceeded */ public void call(T context, int cost) throws SpillwayLimitExceededException { List exceededLimits = getExceededLimits(context, cost); if (!exceededLimits.isEmpty()) { throw new SpillwayLimitExceededException(exceededLimits, context, cost); } } /** * Behave like {@link #tryCall(Object, int)} with {@code cost} of one. * * @see #tryCall(Object, int) * * @param context Either the name of the limit OR the object on which the propertyExtractor ({@link LimitBuilder#of(String, java.util.function.Function)}) * will be applied if it was specified * @return False if one the enforced limit is exceeded, true otherwise */ public boolean tryCall(T context) { return tryCall(context, 1); } /** * Verify if the query should be throttled using the specified cost. * * @param context Either the name of the limit OR the object on which the propertyExtractor ({@link LimitBuilder#of(String, java.util.function.Function)}) * will be applied if it was specified * @param cost The cost of the query * @return False if one the enforced limit is exceeded, true otherwise */ public boolean tryCall(T context, int cost) { return getExceededLimits(context, cost).isEmpty(); } private List getExceededLimits(T context, int cost) { Instant now = Instant.now(clock); List requests = buildRequestsFromLimits(context, 0, now); Map results = storage.addAndGet(requests); List exceededLimits = new ArrayList<>(); if (results.size() == limits.size()) { for (Entry result : results.entrySet()) { Limit limit = limits .stream() .filter(entry -> entry.getName().equals(result.getKey().getLimitName())) .findFirst() .get(); handleTriggers(context, cost, now, result.getValue() + cost, limit); if (result.getValue() + cost > limit.getCapacity(context)) { exceededLimits.add(limit.getDefinition()); } } } else { logger.error( "Something went very wrong. We sent {} limits to the backend but received {} responses. Assuming that no limits were exceeded. Limits: {}. Results: {}.", limits.size(), results.size(), limits, results); } if (exceededLimits.isEmpty()) { requests = buildRequestsFromLimits(context, cost, now); storage.addAndGet(requests); } return exceededLimits; } private List buildRequestsFromLimits(T context, int cost, Instant now) { return limits .stream() .map( limit -> new AddAndGetRequest.Builder() .withResource(resource) .withLimitName(limit.getName()) .withProperty(limit.getProperty(context)) .withDistributed(limit.isDistributed()) .withExpiration(limit.getExpiration(context)) .withEventTimestamp(now) .withCost(cost) .build()) .collect(Collectors.toList()); } private void handleTriggers( T context, int cost, Instant timestamp, int currentValue, Limit limit) { for (LimitTrigger trigger : limit.getLimitTriggers(context)) { try { trigger.callbackIfRequired( context, cost, timestamp, currentValue, limit.getDefinition(context)); } catch (RuntimeException ex) { logger.warn( "Trigger callback {} for limit {} threw an exception. Ignoring.", trigger, limit, ex); } } } /** * This is a costly operation that should only be used for debugging. * Limits should always be enforced through the call and tryCall methods. * * @return Every limit and its current associated counter. */ public Map debugCurrentLimitCounters() { return storage.getCurrentLimitCounters(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy