jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/367664 )

Change subject: throttling filter to protect access to sparql endpoint
......................................................................


throttling filter to protect access to sparql endpoint

This provides a way to throttle client which consume too much resources.

See the description in the ThrittlingFilter class for details.

Bug: T170860
Change-Id: If3c0c28c47f953fdb7f3b6186da8a9535cc18bdf
---
M blazegraph/pom.xml
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Bucketing.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilter.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Throttler.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingFilter.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingState.java
A 
blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketing.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilterTest.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlerTest.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingStateTest.java
A 
blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketingTest.java
M pom.xml
M war/src/main/webapp/WEB-INF/web.xml
13 files changed, 1,162 insertions(+), 1 deletion(-)

Approvals:
  Smalyshev: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/blazegraph/pom.xml b/blazegraph/pom.xml
index 86187ef..5318026 100644
--- a/blazegraph/pom.xml
+++ b/blazegraph/pom.xml
@@ -74,6 +74,10 @@
       <artifactId>jetty-http</artifactId>
     </dependency>
     <dependency>
+      <groupId>org.isomorphism</groupId>
+      <artifactId>token-bucket</artifactId>
+    </dependency>
+    <dependency>
       <groupId>org.linkeddatafragments</groupId>
       <artifactId>ldfserver</artifactId>
       <classifier>classes</classifier>
@@ -127,6 +131,21 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.wikidata.query.rdf</groupId>
       <artifactId>testTools</artifactId>
       <scope>test</scope>
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Bucketing.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Bucketing.java
new file mode 100644
index 0000000..ace9eeb
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Bucketing.java
@@ -0,0 +1,22 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Segmentation of requests.
+ *
+ * Resource consumption is done by <i>client</i>. This interface defines how we
+ * segment clients in different buckets.
+ *
+ * @param <T> the type of the bucket identifier
+ */
+public interface Bucketing<T> {
+    /**
+     * Compute a identifier for the bucket in which this request needs to be
+     * stored.
+     *
+     * @param request the request for which to compute the bucket
+     * @return an object identifying the bucket
+     */
+    T bucket(HttpServletRequest request);
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilter.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilter.java
new file mode 100644
index 0000000..ce2e67e
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilter.java
@@ -0,0 +1,80 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Wrap {@link HttpServletRequest} so that it honors the "X-Real-IP" header.
+ */
+public class ClientIPFilter implements Filter {
+
+    /** Header name for X-Client-IP. */
+    private static final String X_CLIENT_IP = "X-Client-IP";
+
+    /** {@inheritDoc} */
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        // Do nothing
+    }
+
+    /**
+     * Wrap {@link HttpServletRequest} so that it honors the "X-Real-IP" 
header.
+     *
+     * @param request {@inheritDoc}
+     * @param response {@inheritDoc}
+     * @param chain {@inheritDoc}
+     * @throws IOException {@inheritDoc}
+     * @throws ServletException {@inheritDoc}
+     */
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
+            throws IOException, ServletException {
+        if (request instanceof HttpServletRequest) {
+            chain.doFilter(new RealIPHttpRequestWrapper((HttpServletRequest) 
request), response);
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void destroy() {
+        // Do nothing
+    }
+
+
+    /** Wrapping the request and implementing lookup of remoteAddr with 
x-client-ip header. */
+    private static final class RealIPHttpRequestWrapper extends 
HttpServletRequestWrapper {
+
+        /** Constructor. */
+        private RealIPHttpRequestWrapper(HttpServletRequest request) {
+            super(request);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getRemoteAddr() {
+            String realIP = getHeader(X_CLIENT_IP);
+            return realIP != null ? realIP : super.getRemoteAddr();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String getRemoteHost() {
+            try {
+                return InetAddress.getByName(getRemoteAddr()).getHostName();
+            } catch (UnknownHostException e) {
+                return getRemoteAddr();
+            }
+        }
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Throttler.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Throttler.java
new file mode 100644
index 0000000..da237d2
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/Throttler.java
@@ -0,0 +1,131 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import com.google.common.cache.Cache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+
+import static java.time.temporal.ChronoUnit.MILLIS;
+
+/**
+ * Implement throttling logic.
+ *
+ * @see ThrottlingFilter for a more complete description of how throttling
+ * works.
+ * @param <B> type of the bucket used to differentiate clients
+ */
+public class Throttler<B> {
+
+    private static final Logger log = LoggerFactory.getLogger(Throttler.class);
+
+    /** How to associate a request with a specific bucket. */
+    private final Bucketing<B> bucketing;
+    /**
+     * Stores the throttling state by buckets.
+     *
+     * This is a slight abuse of Guava {@link Cache}, but makes it easy to have
+     * an LRU map with an automatic cleanup mechanism.
+     */
+    // TODO: we probably want to expose metrics on the size / usage of this 
cache
+    private final Cache<B, ThrottlingState> state;
+    /** Requests longer than this will trigger tracking resource consumption. 
*/
+    private final Duration requestTimeThreshold;
+    /** How to create the initial throttling state when we start tracking a 
specific client. */
+    private final Callable<ThrottlingState> createThrottlingState;
+
+    /**
+     * Constructor.
+     *
+     * Note that a bucket represent our approximation of a single client.
+     *
+     * @param requestTimeThreshold requests longer than this will trigger
+     *                             tracking resource consumption
+     * @param bucketing how to associate a request with a specific bucket
+     * @param createThrottlingState how to create the initial throttling state
+     *                              when we start tracking a specific client
+     * @param stateStore the cache in which we store the per client state of
+     *                   throttling
+     */
+    public Throttler(
+            Duration requestTimeThreshold,
+            Bucketing<B> bucketing,
+            Callable<ThrottlingState> createThrottlingState,
+            Cache<B, ThrottlingState> stateStore) {
+        this.requestTimeThreshold = requestTimeThreshold;
+        this.bucketing = bucketing;
+        this.state = stateStore;
+        this.createThrottlingState = createThrottlingState;
+    }
+
+    /**
+     * Should this request be throttled.
+     *
+     * @param request the request to check
+     * @return true if the request should be throttled
+     */
+    public boolean isThrottled(HttpServletRequest request) {
+        ThrottlingState throttlingState = 
state.getIfPresent(bucketing.bucket(request));
+        if (throttlingState == null) return false;
+
+        return throttlingState.isThrottled();
+    }
+
+    /**
+     * Notify this throttler that a request has been completed successfully.
+     *
+     * @param request the request
+     * @param elapsed how long that request took
+     */
+    public void success(HttpServletRequest request, Duration elapsed) {
+        try {
+            B bucket = bucketing.bucket(request);
+            ThrottlingState throttlingState;
+            // only start to keep track of time usage if requests are expensive
+            if (elapsed.compareTo(requestTimeThreshold) > 0) {
+                throttlingState = state.get(bucket, createThrottlingState);
+            } else {
+                throttlingState = state.getIfPresent(bucket);
+            }
+            if (throttlingState != null) {
+                throttlingState.consumeTime(elapsed);
+            }
+        } catch (ExecutionException ee) {
+            log.warn("Could not create throttling state", ee);
+        }
+    }
+
+    /**
+     * Notify this throttler that a request has completed in error.
+     *
+     * @param request the request
+     * @param elapsed how long that request took
+     */
+    public void failure(HttpServletRequest request, Duration elapsed) {
+        try {
+            ThrottlingState throttlingState = 
state.get(bucketing.bucket(request), createThrottlingState);
+
+            throttlingState.consumeError();
+            throttlingState.consumeTime(elapsed);
+        } catch (ExecutionException ee) {
+            log.warn("Could not create throttling state", ee);
+        }
+    }
+
+    /**
+     * How long should this client wait before his next request.
+     *
+     * @param request the request
+     * @return 0 if no throttling, the backoff delay otherwise
+     */
+    public Duration getBackoffDelay(HttpServletRequest request) {
+        ThrottlingState throttlingState = 
state.getIfPresent(bucketing.bucket(request));
+        if (throttlingState == null) return Duration.of(0, MILLIS);
+
+        return throttlingState.getBackoffDelay();
+    }
+
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingFilter.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingFilter.java
new file mode 100644
index 0000000..72a1198
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingFilter.java
@@ -0,0 +1,268 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+
+import com.google.common.base.Stopwatch;
+import com.google.common.cache.CacheBuilder;
+import org.isomorphism.util.TokenBuckets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import 
org.wikidata.query.rdf.blazegraph.throttling.UserAgentIpAddressBucketing.Bucket;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.Boolean.parseBoolean;
+import static java.lang.Integer.parseInt;
+import static java.lang.String.format;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static java.util.Locale.ENGLISH;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * A Servlet Filter that applies throttling.
+ *
+ * The throttling is based on the request time consumed and the number of
+ * errors. The rational is:
+ *
+ * <dl>
+ *     <dt>request time</dt>
+ *     <dd>This is a good proxy for how much resource (CPU, IO) are 
consumed.</dd>
+ *     <dt>errors</dt>
+ *     <dd>A client always in error indicates a problem client side, which
+ *     should be fixed client side.</dd>
+ * </dl>
+ *
+ * Resource consumption is based on <a
+ * href="https://en.wikipedia.org/wiki/Token_bucket";>token buckets</a> as
+ * implemented by <a href="https://github.com/bbeck/token-bucket";>bbeck</a>. A
+ * token bucket is defined by:
+ *
+ * <dl>
+ *     <dt>capacity</dt>
+ *     <dd>the maximum number of tokes in the bucket</dd>
+ *     <dt>refill amount</dt>
+ *     <dd>the number of tokens to add to the bucket when refilling</dd>
+ *     <dt>refill period</dt>
+ *     <dd>how often to refill the bucket</dd>
+ * </dl>
+ *
+ * This filter has two buckets, one to keep track of time, and one to keep
+ * track of errors. Each time an error occurs, a token is taken out of the
+ * error bucket. Each refill period, tokens are added again. The time bucket
+ * has a similar behaviour. As an optimization, we start keeping track of
+ * resource consumption only if:
+ *
+ * <ol>
+ *     <li>a request is taking a significant time</li>
+ *     <li>a request is in error</li>
+ * </ol>
+ *
+ * The client is throttled if either the time bucket or the error bucket is
+ * empty. Since we don't know in advance the cost of a request or if it is
+ * going to be in error, the throttling will only occur for the next requests.
+ *
+ * In case of throttling, the client is notified by an HTTP 429 status code and
+ * is presented with a <code>Retry-After</code> HTTP header giving a backoff
+ * time in seconds.
+ *
+ * The clients are segmented in different buckets and resource consumption is
+ * tracked individually for each of those buckets. The segmentation is done by
+ * [IP address, User Agent], but could be extended to support more complex
+ * strategies. A bucket is only kept while its client is active. After a period
+ * of inactivity, the bucket is deleted.
+ *
+ * All state is limited to a single JVM, this filter is not cluster aware.
+ */
+public class ThrottlingFilter implements Filter {
+
+    private static final Logger log = 
LoggerFactory.getLogger(ThrottlingFilter.class);
+
+    /** Is throttling enabled. */
+    private boolean enabled;
+    /** To delegate throttling logic. */
+    private Throttler<Bucket> throttler;
+
+    /**
+     * Initialise the filter.
+     *
+     * Configuration is loaded from TODO.
+     * @param filterConfig {@inheritDoc}
+     */
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        // TODO: the value of the parameters are mostly invented, we need to
+        // find the correct ones.
+
+        int requestDurationThresholdInSeconds = 
loadIntParam("request-duration-threshold-in-seconds", filterConfig, 10);
+        int timeBucketCapacityInSeconds = 
loadIntParam("time-bucket-capacity-in-seconds", filterConfig, 60);
+        int timeBucketRefillAmountInSeconds = 
loadIntParam("time-bucket-refill-amount-in-seconds", filterConfig, 60);
+        int timeBucketRefillPeriodInMinutes = 
loadIntParam("time-bucket-refill-period-in-minutes", filterConfig, 1);
+        int errorBucketCapacity = loadIntParam("error-bucket-capacity", 
filterConfig, 10);
+        int errorBucketRefillAmount = 
loadIntParam("error-bucket-refill-amount", filterConfig, 100);
+        int errorBucketRefillPeriodInMinutes = 
loadIntParam("error-bucket-refill-period-in-minutes", filterConfig, 1);
+        int maxStateSize = loadIntParam("max-state-size", filterConfig, 10000);
+        int stateExpirationInMinutes = 
loadIntParam("state-expiration-in-minutes", filterConfig, 15);
+
+        this.enabled = loadBooleanParam("enabled", filterConfig, true);
+        throttler = new Throttler<>(
+                Duration.of(requestDurationThresholdInSeconds, SECONDS),
+                new UserAgentIpAddressBucketing(),
+                createThrottlingState(
+                        timeBucketCapacityInSeconds,
+                        timeBucketRefillAmountInSeconds,
+                        timeBucketRefillPeriodInMinutes,
+                        errorBucketCapacity,
+                        errorBucketRefillAmount,
+                        errorBucketRefillPeriodInMinutes),
+                CacheBuilder.newBuilder()
+                        .maximumSize(maxStateSize)
+                        .expireAfterAccess(stateExpirationInMinutes, 
TimeUnit.MINUTES)
+                        .build());
+    }
+
+    /**
+     * See {@link ThrottlingFilter#loadStringParam(String, FilterConfig)}.
+     *
+     * @param name
+     * @param filterConfig
+     * @param defaultValue
+     * @return
+     */
+    private int loadIntParam(String name, FilterConfig filterConfig, int 
defaultValue) {
+        String result = loadStringParam(name, filterConfig);
+        return result != null ? parseInt(result) : defaultValue;
+    }
+
+    /**
+     * See {@link ThrottlingFilter#loadStringParam(String, FilterConfig)}.
+     *
+     * @param name
+     * @param filterConfig
+     * @param defaultValue
+     * @return
+     */
+    private boolean loadBooleanParam(String name, FilterConfig filterConfig, 
boolean defaultValue) {
+        String result = loadStringParam(name, filterConfig);
+        return result != null ? parseBoolean(result) : defaultValue;
+    }
+
+    /**
+     * Load a parameter from multiple locations.
+     *
+     * System properties have the highest priority, filter config is used if no
+     * system property is found.
+     *
+     * The system property used is 
<code>wdqs.&lt;filter-name&gt;.&lt;name&gt;</code>.
+     *
+     * @param name name of the property
+     * @param filterConfig used to get the filter config
+     * @return the value of the parameter
+     */
+    private String loadStringParam(String name, FilterConfig filterConfig) {
+        String result = null;
+        String fParam = filterConfig.getInitParameter(name);
+        if (fParam != null) {
+            result = fParam;
+        }
+        String sParam = System.getProperty("wdqs." + 
filterConfig.getFilterName() + "." + name);
+        if (sParam != null) {
+            result = sParam;
+        }
+        return result;
+    }
+
+    /**
+     * Create Callable to initialize throttling state.
+     *
+     * @param timeBucketCapacityInSeconds
+     * @param timeBucketRefillAmountInSeconds
+     * @param timeBucketRefillPeriodInMinutes
+     * @param errorBucketCapacity
+     * @param errorBucketRefillAmount
+     * @param errorBucketRefillPeriodInMinutes
+     */
+    public static Callable<ThrottlingState> createThrottlingState(
+            int timeBucketCapacityInSeconds,
+            int timeBucketRefillAmountInSeconds,
+            int timeBucketRefillPeriodInMinutes,
+            int errorBucketCapacity,
+            int errorBucketRefillAmount,
+            int errorBucketRefillPeriodInMinutes) {
+        return () -> new ThrottlingState(
+                TokenBuckets.builder()
+                        .withCapacity(
+                                
MILLISECONDS.convert(timeBucketCapacityInSeconds, TimeUnit.SECONDS))
+                        .withFixedIntervalRefillStrategy(
+                                
MILLISECONDS.convert(timeBucketRefillAmountInSeconds, TimeUnit.SECONDS),
+                                timeBucketRefillPeriodInMinutes, MINUTES)
+                        .build(),
+                TokenBuckets.builder()
+                        .withCapacity(errorBucketCapacity)
+                        .withFixedIntervalRefillStrategy(
+                                errorBucketRefillAmount,
+                                errorBucketRefillPeriodInMinutes, MINUTES)
+                        .build());
+    }
+
+    /**
+     * Check resource consumption and throttle requests as needed.
+     *
+     * @param request {@inheritDoc}
+     * @param response {@inheritDoc}
+     * @param chain {@inheritDoc}
+     * @throws IOException {@inheritDoc}
+     * @throws ServletException {@inheritDoc}
+     */
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        if (throttler.isThrottled(httpRequest)) {
+            log.info("A request is being throttled.");
+            if (enabled) {
+                notifyUser(httpResponse, 
throttler.getBackoffDelay(httpRequest));
+                return;
+            }
+        }
+
+        Stopwatch stopwatch = Stopwatch.createStarted();
+        try {
+            chain.doFilter(request, response);
+            throttler.success(httpRequest, stopwatch.elapsed());
+        } catch (IOException | ServletException e) {
+            throttler.failure(httpRequest, stopwatch.elapsed());
+            throw e;
+        }
+    }
+
+    /**
+     * Notify the user that he is being throttled.
+     *
+     * @param response the response
+     * @param backoffDelay the backoff delay
+     * @throws IOException if the response cannot be written
+     */
+    private void notifyUser(HttpServletResponse response, Duration 
backoffDelay) throws IOException {
+        String retryAfter = Long.toString(backoffDelay.getSeconds());
+        response.setHeader("Retry-After", retryAfter);
+        response.sendError(429, format(ENGLISH, "Too Many Requests - Please 
retry in %s seconds.", retryAfter));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void destroy() {
+        // Nothing to destroy
+    }
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingState.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingState.java
new file mode 100644
index 0000000..58e552e
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingState.java
@@ -0,0 +1,90 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import org.isomorphism.util.TokenBucket;
+
+import java.time.Duration;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+import static java.time.temporal.ChronoUnit.MILLIS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * Keeps track of resource useage for a client.
+ *
+ * This class tracks of the request time consumed and of the errors.
+ */
+public final class ThrottlingState {
+
+    /** Bucket to track requests time. */
+    private final TokenBucket timeBucket;
+    /** Bucket to track errors. */
+    private final TokenBucket errorsBucket;
+
+    /**
+     * Constructor.
+     *
+     * @param timeBucket bucket to track requests time
+     * @param errorsBucket bucket to track errors
+     */
+    public ThrottlingState(TokenBucket timeBucket, TokenBucket errorsBucket) {
+        this.timeBucket = timeBucket;
+        this.errorsBucket = errorsBucket;
+    }
+
+    /**
+     * Should this client be throttled.
+     */
+    public synchronized boolean isThrottled() {
+        return timeBucket.getNumTokens() == 0 || errorsBucket.getNumTokens() 
== 0;
+    }
+
+    /**
+     * How long should a client wait before sending the next request.
+     *
+     * @return 0 if the client is not throttled, or the backoff delay 
otherwise.
+     */
+    public synchronized Duration getBackoffDelay() {
+        return Duration.of(
+                max(backoffDelayMillis(timeBucket), 
backoffDelayMillis(errorsBucket)),
+                MILLIS
+        );
+    }
+
+    /**
+     * Consumes request time from the time bucket.
+     *
+     * If the time bucket contains less tokens than what the request consumed,
+     * all available tokens will be consumed:
+     * <code>min(elapsed.toMillis(), timeBucket.getNumTokens())</code>.
+     *
+     * @param elapsed time elapsed during the request
+     */
+    public synchronized void consumeTime(Duration elapsed) {
+        long tokenToConsume = min(elapsed.toMillis(), 
timeBucket.getNumTokens());
+        // consuming zero tokens is not allowed (and would be a NOOP anyway)
+        if (tokenToConsume > 0) {
+            timeBucket.consume(tokenToConsume);
+        }
+    }
+
+    /**
+     * Consumes errors from the error bucket.
+     *
+     * If the error bucket is already empty, does nothing.
+     */
+    public synchronized void consumeError() {
+        errorsBucket.tryConsume();
+    }
+
+    /**
+     * How long to wait before next refill for the specified bucket.
+     * @param bucket the bucket to check
+     * @return 0 if the bucket is not empty, the delay otherwise
+     */
+    private static long backoffDelayMillis(TokenBucket bucket) {
+        if (bucket.getNumTokens() > 0) return 0;
+        return bucket.getDurationUntilNextRefill(MILLISECONDS);
+    }
+
+}
diff --git 
a/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketing.java
 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketing.java
new file mode 100644
index 0000000..2dad56a
--- /dev/null
+++ 
b/blazegraph/src/main/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketing.java
@@ -0,0 +1,58 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Objects;
+
+/**
+ * Segmentation of requests by user-agent and IP address.
+ */
+public class UserAgentIpAddressBucketing implements 
Bucketing<UserAgentIpAddressBucketing.Bucket> {
+
+    /** {@inheritDoc} */
+    @Override
+    public Bucket bucket(HttpServletRequest request) {
+        return new Bucket(request.getRemoteAddr(), 
request.getHeader("User-Agent"));
+    }
+
+    /**
+     * A bucket based on user-agent and IP address.
+     *
+     * This is a simple class to wrap the user agent and IP address to avoid
+     * doing string concatenation.
+     */
+    public static final class Bucket {
+        /** IP address. */
+        private final String remoteAddr;
+        /** User-agent. */
+        private final String userAgent;
+
+        /**
+         * Constructor.
+         *
+         * @param remoteAddr IP address
+         * @param userAgent user-agent
+         */
+        private Bucket(String remoteAddr, String userAgent) {
+            this.remoteAddr = remoteAddr;
+            this.userAgent = userAgent;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Bucket bucket = (Bucket) o;
+
+            return Objects.equals(remoteAddr, bucket.remoteAddr)
+                    && Objects.equals(userAgent, bucket.userAgent);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int hashCode() {
+            return Objects.hash(remoteAddr, userAgent);
+        }
+    }
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilterTest.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilterTest.java
new file mode 100644
index 0000000..7001ac8
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ClientIPFilterTest.java
@@ -0,0 +1,83 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClientIPFilterTest {
+
+    private Filter filter = new ClientIPFilter();
+    @Mock private HttpServletResponse response;
+    @Mock private FilterChain chain;
+    @Captor private ArgumentCaptor<ServletRequest> filteredRequest;
+
+    @Test
+    public void remoteAddrIsReturnedWhenNoRealIpHeaderIsPresent() throws 
Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setRemoteAddr("1.2.3.4");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(filteredRequest.capture(), 
any(ServletResponse.class));
+        assertThat(filteredRequest.getValue().getRemoteAddr(), 
equalTo("1.2.3.4"));
+    }
+
+    @Test
+    public void realIpIsReturnedWhenHeaderIsPresent() throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setRemoteAddr("1.2.3.4");
+        request.addHeader("X-Client-IP", "4.3.2.1");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(filteredRequest.capture(), 
any(ServletResponse.class));
+        assertThat(filteredRequest.getValue().getRemoteAddr(), 
equalTo("4.3.2.1"));
+    }
+
+    @Test
+    public void caseOfHeaderIsIgnored() throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setRemoteAddr("1.2.3.4");
+        request.addHeader("x-client-ip", "4.3.2.1");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(filteredRequest.capture(), 
any(ServletResponse.class));
+        assertThat(filteredRequest.getValue().getRemoteAddr(), 
equalTo("4.3.2.1"));
+    }
+
+    @Test
+    public void dontWrapNonHttpRequests() throws IOException, ServletException 
{
+        ServletRequest request = mock(ServletRequest.class);
+        ServletResponse response = mock(ServletResponse.class);
+        ArgumentCaptor<ServletRequest> filteredRequest = 
ArgumentCaptor.forClass(ServletRequest.class);
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(filteredRequest.capture(), 
any(ServletResponse.class));
+
+        assertThat(filteredRequest.getValue(), sameInstance(request));
+    }
+
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlerTest.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlerTest.java
new file mode 100644
index 0000000..53b0a5a
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlerTest.java
@@ -0,0 +1,168 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import org.isomorphism.util.TokenBuckets;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import 
org.wikidata.query.rdf.blazegraph.throttling.UserAgentIpAddressBucketing.Bucket;
+
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ThrottlerTest {
+
+    private Cache<Bucket, ThrottlingState> stateStore;
+    private Throttler throttler;
+
+    @Before
+    public void createThrottlerUnderTest() {
+        stateStore = CacheBuilder.newBuilder()
+                .maximumSize(10000)
+                .expireAfterAccess(15, TimeUnit.MINUTES)
+                .build();
+        throttler = new Throttler<>(
+                Duration.of(20, SECONDS),
+                new UserAgentIpAddressBucketing(),
+                createThrottlingState(),
+                stateStore);
+    }
+
+    private static Callable<ThrottlingState> createThrottlingState() {
+        return () -> new ThrottlingState(
+                TokenBuckets.builder()
+                        .withCapacity(MILLISECONDS.convert(40, 
TimeUnit.SECONDS))
+                        .withFixedIntervalRefillStrategy(1000000, 1, MINUTES)
+                        .build(),
+                TokenBuckets.builder()
+                        .withCapacity(10)
+                        .withFixedIntervalRefillStrategy(100, 1, MINUTES)
+                        .build());
+    }
+
+    @Test
+    public void newClientIsNotThrottled() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        assertFalse(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void shortRequestsDoNotCreateState() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.success(request, Duration.of(10, SECONDS));
+
+        assertThat(stateStore.size(), equalTo(0L));
+    }
+
+    @Test
+    public void backoffDelayIsZeroForNewClient() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        assertThat(throttler.getBackoffDelay(request), equalTo(Duration.of(0, 
SECONDS)));
+    }
+
+    @Test
+    public void 
requestOverThresholdButBelowThrottlingRateEnablesUserTracking() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.success(request, Duration.of(30, SECONDS));
+
+        assertThat(stateStore.size(), equalTo(1L));
+        assertFalse(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void requestOverThrottlingRateWillThrottleNextRequest() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.success(request, Duration.of(60, SECONDS));
+
+        assertTrue(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void backoffDelayIsGivenForTimeThrottledClient() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.success(request, Duration.of(60, SECONDS));
+
+        Duration backoffDelay = throttler.getBackoffDelay(request);
+        assertThat(backoffDelay.compareTo(Duration.of(0, SECONDS)), 
greaterThan(0));
+        assertThat(backoffDelay.compareTo(Duration.of(60, SECONDS)), 
lessThan(0));
+    }
+
+    @Test
+    public void 
onceTrackingIsEnabledEvenShortRequestsAreTrackedAndEnableThrottling() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.success(request, Duration.of(30, SECONDS));
+
+        assertThat(stateStore.size(), equalTo(1L));
+        assertFalse(throttler.isThrottled(request));
+
+        for (int i = 0; i < 200; i++) {
+            throttler.success(request, Duration.of(1, SECONDS));
+        }
+
+        assertThat(stateStore.size(), equalTo(1L));
+        assertTrue(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void errorEnablesTrackingOfRequests() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        throttler.failure(request, Duration.of(10, SECONDS));
+
+        assertThat(stateStore.size(), equalTo(1L));
+        assertFalse(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void multipleErrorsEnablesThrottling() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        for (int i = 0; i < 100; i++) {
+            throttler.failure(request, Duration.of(10, SECONDS));
+        }
+
+        assertThat(stateStore.size(), equalTo(1L));
+        assertTrue(throttler.isThrottled(request));
+    }
+
+    @Test
+    public void backoffDelayIsGivenForErrorThrottledClient() {
+        MockHttpServletRequest request = createRequest("UA1", "1.2.3.4");
+
+        for (int i = 0; i < 100; i++) {
+            throttler.failure(request, Duration.of(10, SECONDS));
+        }
+
+        Duration backoffDelay = throttler.getBackoffDelay(request);
+        assertThat(backoffDelay.compareTo(Duration.of(0, SECONDS)), 
greaterThan(0));
+        assertThat(backoffDelay.compareTo(Duration.of(60, SECONDS)), 
lessThan(0));
+    }
+
+
+    private MockHttpServletRequest createRequest(String userAgent, String 
remoteAddr) {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("User-Agent", userAgent);
+        request.setRemoteAddr(remoteAddr);
+        return request;
+    }
+
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingStateTest.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingStateTest.java
new file mode 100644
index 0000000..32af484
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/ThrottlingStateTest.java
@@ -0,0 +1,121 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import org.isomorphism.util.TokenBucket;
+import org.isomorphism.util.TokenBuckets;
+import org.junit.Test;
+
+import java.time.Duration;
+
+import static java.time.temporal.ChronoUnit.MILLIS;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ThrottlingStateTest {
+
+    @Test
+    public void fullBucketsAreNotThrottled() {
+        assertTrue(!new ThrottlingState(fullBucket(), 
fullBucket()).isThrottled());
+    }
+
+    @Test
+    public void anyEmptyBucketIsThrottled() {
+        assertTrue(new ThrottlingState(fullBucket(), 
emptyBucket()).isThrottled());
+        assertTrue(new ThrottlingState(emptyBucket(), 
fullBucket()).isThrottled());
+        assertTrue(new ThrottlingState(emptyBucket(), 
emptyBucket()).isThrottled());
+
+    }
+
+    @Test
+    public void backoffDelayIsZeroWhenBucketsAreNonEmpty() {
+        ThrottlingState state = new ThrottlingState(fullBucket(), 
fullBucket());
+        assertThat(state.getBackoffDelay(), equalTo(Duration.of(0, SECONDS)));
+    }
+
+    @Test
+    public void backoffDelayIsTheLargestDelay() {
+        TokenBucket emptyBucket1 = mock(TokenBucket.class);
+        when(emptyBucket1.getNumTokens()).thenReturn(0L);
+        
when(emptyBucket1.getDurationUntilNextRefill(MILLISECONDS)).thenReturn(1000L);
+
+        TokenBucket emptyBucket2 = mock(TokenBucket.class);
+        when(emptyBucket2.getNumTokens()).thenReturn(0L);
+        
when(emptyBucket2.getDurationUntilNextRefill(MILLISECONDS)).thenReturn(10000L);
+
+        assertThat(
+                new ThrottlingState(fullBucket(), 
emptyBucket1).getBackoffDelay(),
+                equalTo(Duration.of(1, SECONDS)));
+
+        assertThat(
+                new ThrottlingState(emptyBucket1, 
emptyBucket2).getBackoffDelay(),
+                equalTo(Duration.of(10, SECONDS)));
+    }
+
+    @Test
+    public void canConsumeTime() {
+        TokenBucket timeBucket = fullBucket();
+        ThrottlingState state = new ThrottlingState(timeBucket, fullBucket());
+        long tokensBefore = timeBucket.getNumTokens();
+
+        state.consumeTime(Duration.of(500, MILLIS));
+
+        assertThat(timeBucket.getNumTokens() + 500, equalTo(tokensBefore));
+    }
+
+    @Test
+    public void canConsumeTimeEvenIfNotEnoughTokensAvailable() {
+        TokenBucket timeBucket = fullBucket();
+        ThrottlingState state = new ThrottlingState(timeBucket, fullBucket());
+
+        state.consumeTime(Duration.of(5, SECONDS));
+
+        assertThat(timeBucket.getNumTokens(), equalTo(0L));
+    }
+
+    @Test
+    public void canConsumeErrors() {
+        TokenBucket errorsBucket = fullBucket();
+        ThrottlingState state = new ThrottlingState(fullBucket(), 
errorsBucket);
+        long tokensBefore = errorsBucket.getNumTokens();
+
+        state.consumeError();
+
+        assertThat(errorsBucket.getNumTokens() + 1, equalTo(tokensBefore));
+    }
+
+    @Test
+    public void canConsumeErrorsEvenIfNotEnoughTokensAvailable() {
+        TokenBucket errorsBucket = emptyBucket();
+        ThrottlingState state = new ThrottlingState(errorsBucket, 
fullBucket());
+
+        state.consumeError();
+
+        assertThat(errorsBucket.getNumTokens(), equalTo(0L));
+    }
+
+    private TokenBucket fullBucket() {
+        return TokenBuckets.builder()
+                .withCapacity(1000)
+                .withFixedIntervalRefillStrategy(1000, 1, MINUTES)
+                .build();
+    }
+
+    private TokenBucket emptyBucket() {
+        TokenBucket bucket = TokenBuckets.builder()
+                .withCapacity(1000)
+                .withFixedIntervalRefillStrategy(1000, 1, MINUTES)
+                .build();
+        while (bucket.tryConsume()) {
+            // consume all tokens
+        }
+        return bucket;
+    }
+
+
+
+}
diff --git 
a/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketingTest.java
 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketingTest.java
new file mode 100644
index 0000000..f8c90ee
--- /dev/null
+++ 
b/blazegraph/src/test/java/org/wikidata/query/rdf/blazegraph/throttling/UserAgentIpAddressBucketingTest.java
@@ -0,0 +1,76 @@
+package org.wikidata.query.rdf.blazegraph.throttling;
+
+import org.junit.Before;
+import org.junit.Test;
+import 
org.wikidata.query.rdf.blazegraph.throttling.UserAgentIpAddressBucketing.Bucket;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UserAgentIpAddressBucketingTest {
+
+    private UserAgentIpAddressBucketing bucketting;
+
+    @Before
+    public void createBuckettingUnderTest() {
+        bucketting = new UserAgentIpAddressBucketing();
+    }
+
+    @Test
+    public void sameUserAgentAndIpAddressAreInTheSameBucket() {
+        Bucket bucket1 = bucketting.bucket(mockRequest("1.2.3.4", "UA1"));
+        Bucket bucket2 = bucketting.bucket(mockRequest("1.2.3.4", "UA1"));
+
+        assertThat(bucket1, equalTo(bucket2));
+        assertThat(bucket1.hashCode(), equalTo(bucket2.hashCode()));
+    }
+
+    @Test
+    public void nullUserAgentAndNonNullIpAddressAreInTheSameBucket() {
+        Bucket bucket1 = bucketting.bucket(mockRequest("1.2.3.4", null));
+        Bucket bucket2 = bucketting.bucket(mockRequest("1.2.3.4", null));
+
+        assertThat(bucket1, equalTo(bucket2));
+        assertThat(bucket1.hashCode(), equalTo(bucket2.hashCode()));
+    }
+
+    @Test
+    public void nullUserAgentAndNullIpAddressAreInTheSameBucket() {
+        Bucket bucket1 = bucketting.bucket(mockRequest(null, null));
+        Bucket bucket2 = bucketting.bucket(mockRequest(null, null));
+
+        assertThat(bucket1, equalTo(bucket2));
+        assertThat(bucket1.hashCode(), equalTo(bucket2.hashCode()));
+    }
+
+    @Test
+    public void differentUserAgentsAreInDifferentBuckets() {
+        Bucket bucket1 = bucketting.bucket(mockRequest("1.2.3.4", "UA1"));
+        Bucket bucket2 = bucketting.bucket(mockRequest("1.2.3.4", "UA2"));
+
+        assertThat(bucket1, not(equalTo(bucket2)));
+        assertThat(bucket1.hashCode(), not(equalTo(bucket2.hashCode())));
+    }
+
+    @Test
+    public void differentIpAddressesAreInDifferentBuckets() {
+        Bucket bucket1 = bucketting.bucket(mockRequest("1.2.3.4", "UA1"));
+        Bucket bucket2 = bucketting.bucket(mockRequest("4.3.2.1", "UA1"));
+
+        assertThat(bucket1, not(equalTo(bucket2)));
+        assertThat(bucket1.hashCode(), not(equalTo(bucket2.hashCode())));
+    }
+
+    private HttpServletRequest mockRequest(String ipAddress, String userAgent) 
{
+        HttpServletRequest request1 = mock(HttpServletRequest.class);
+        when(request1.getRemoteAddr()).thenReturn(ipAddress);
+        when(request1.getHeader("User-Agent")).thenReturn(userAgent);
+        return request1;
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 7d65391..866e8a8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -247,7 +247,7 @@
       <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
-        <version>21.0</version>
+        <version>22.0</version>
       </dependency>
       <dependency>
         <groupId>com.googlecode.json-simple</groupId>
@@ -422,6 +422,11 @@
         <version>${jetty.version}</version>
       </dependency>
       <dependency>
+        <groupId>org.isomorphism</groupId>
+        <artifactId>token-bucket</artifactId>
+        <version>1.6</version>
+      </dependency>
+      <dependency>
         <groupId>org.jolokia</groupId>
         <artifactId>jolokia-jvm</artifactId>
         <version>1.3.1</version>
@@ -460,6 +465,10 @@
           <exclusion>
             <groupId>log4j</groupId>
             <artifactId>log4j</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
           </exclusion>
         </exclusions>
       </dependency>
@@ -600,6 +609,24 @@
         <scope>test</scope>
       </dependency>
       <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-simple</artifactId>
+        <version>${slf4j.version}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-test</artifactId>
+        <version>4.3.10.RELEASE</version>
+        <scope>test</scope>
+        <exclusions>
+          <exclusion>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
         <groupId>org.wikidata.query.rdf</groupId>
         <artifactId>testTools</artifactId>
         <version>${project.version}</version>
diff --git a/war/src/main/webapp/WEB-INF/web.xml 
b/war/src/main/webapp/WEB-INF/web.xml
index 91efc26..b6f4484 100644
--- a/war/src/main/webapp/WEB-INF/web.xml
+++ b/war/src/main/webapp/WEB-INF/web.xml
@@ -84,6 +84,24 @@
   <listener>
    
<listener-class>org.wikidata.query.rdf.blazegraph.WikibaseContextListener</listener-class>
   </listener>
+  <filter>
+      <filter-name>real-ip-filter</filter-name>
+      
<filter-class>org.wikidata.query.rdf.blazegraph.throttling.ClientIPFilter</filter-class>
+  </filter>
+  <filter-mapping>
+    <filter-name>real-ip-filter</filter-name>
+    <url-pattern>/*</url-pattern>
+  </filter-mapping>
+  <filter>
+      <filter-name>throttling-filter</filter-name>
+      
<filter-class>org.wikidata.query.rdf.blazegraph.throttling.ThrottlingFilter</filter-class>
+  </filter>
+  <filter-mapping>
+    <filter-name>throttling-filter</filter-name>
+    <url-pattern>/sparql</url-pattern>
+    <url-pattern>/namespace</url-pattern>
+    <url-pattern>/namespace/*</url-pattern>
+  </filter-mapping>
   <servlet>
    <servlet-name>REST API</servlet-name>
    <display-name>REST API</display-name>

-- 
To view, visit https://gerrit.wikimedia.org/r/367664
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: If3c0c28c47f953fdb7f3b6186da8a9535cc18bdf
Gerrit-PatchSet: 11
Gerrit-Project: wikidata/query/rdf
Gerrit-Branch: master
Gerrit-Owner: Gehel <[email protected]>
Gerrit-Reviewer: Gehel <[email protected]>
Gerrit-Reviewer: Smalyshev <[email protected]>
Gerrit-Reviewer: Volans <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to