blob: 64ec2b12a899880ce4d6b8bcc76d2e62c431ffcd [file] [log] [blame]
// Copyright (C) 2017 The Android Open Source Project
//
// 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.googlesource.gerrit.plugins.quota;
import static com.googlesource.gerrit.plugins.quota.Module.CACHE_NAME_ACCOUNTID;
import static com.googlesource.gerrit.plugins.quota.Module.CACHE_NAME_REMOTEHOST;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.RateLimiter;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.validators.UploadValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.googlesource.gerrit.plugins.quota.Module.Holder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.UploadPack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
public class RateLimitUploadListener implements UploadValidationListener {
private static final int SECONDS_PER_HOUR = 3600;
private static final Logger log =
LoggerFactory.getLogger(RateLimitUploadListener.class);
private static final Method createStopwatchMethod;
private static final Constructor<?> constructor;
static {
try {
Class<?> sleepingStopwatchClass = Class.forName(
"com.google.common.util.concurrent.RateLimiter$SleepingStopwatch");
createStopwatchMethod =
sleepingStopwatchClass.getDeclaredMethod("createFromSystemTimer");
createStopwatchMethod.setAccessible(true);
Class<?> burstyRateLimiterClass = Class.forName(
"com.google.common.util.concurrent.SmoothRateLimiter$SmoothBursty");
constructor = burstyRateLimiterClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
} catch (ClassNotFoundException | NoSuchMethodException e) {
// shouldn't happen
throw new RuntimeException(
"Failed to prepare loading RateLimiter via reflection", e);
}
}
/**
* Create a custom instance of RateLimiter by accessing the non-public
* constructor of the implementation class SmoothRateLimiter.SmoothBursty
* through reflection.
*
* <p>
* RateLimiter's implementation class SmoothRateLimiter.SmoothBursty allows to
* collect permits during idle times which can be used to send bursts of
* requests exceeding the average rate until the stored permits are consumed.
* If the rate per second is 0.2 and you wait 20 seconds you can acquire 4
* permits which in average matches the configured rate limit of 0.2
* requests/second. If the permitted rate is smaller than 1 per second the
* standard implementation doesn't allow any bursts since it hard-codes the
* maximum time which can be used to collect stored permits to 1 second.
*
* <p>
* Build jobs fetching updates from Gerrit are typically triggered by events
* which can arrive in bursts. Hence the standard RateLimiter seems not to be
* the right choice at least for fetch requests where we probably want to
* limit the rate to less than 1 request per second per user.
*
* <p>
* The used constructor can't be accessed through a public method yet hence
* use reflection to instantiate it.
*
* @see "https://github.com/google/guava/issues/1974"
* @param permitsPerSecond the new stable rate of this {@code RateLimiter}
* @param maxBurstSeconds The maximum number of permits that can be saved.
* @return a new RateLimiter
*/
@VisibleForTesting
static RateLimiter createSmoothBurstyRateLimiter(double permitsPerSecond,
double maxBurstSeconds) {
RateLimiter rl;
try {
Object stopwatch = createStopwatchMethod.invoke(null);
rl = (RateLimiter) constructor.newInstance(stopwatch, maxBurstSeconds);
rl.setRate(permitsPerSecond);
} catch (InvocationTargetException | IllegalAccessException
| InstantiationException e) {
// shouldn't happen
throw new RuntimeException(e);
}
return rl;
}
private final Provider<CurrentUser> user;
private final LoadingCache<Account.Id, Holder> limitsPerAccount;
private final LoadingCache<String, Holder> limitsPerRemoteHost;
@Inject
RateLimitUploadListener(Provider<CurrentUser> user,
@Named(CACHE_NAME_ACCOUNTID) LoadingCache<Account.Id, Holder> limitsPerAccount,
@Named(CACHE_NAME_REMOTEHOST) LoadingCache<String, Holder> limitsPerRemoteHost) {
this.user = user;
this.limitsPerAccount = limitsPerAccount;
this.limitsPerRemoteHost = limitsPerRemoteHost;
}
@Override
public void onBeginNegotiate(Repository repository, Project project,
String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
int cntOffered) throws ValidationException {
RateLimiter limiter = null;
CurrentUser u = user.get();
if (u.isIdentifiedUser()) {
Account.Id accountId = u.asIdentifiedUser().getAccountId();
try {
limiter = limitsPerAccount.get(accountId).get();
} catch (ExecutionException e) {
String msg = MessageFormat
.format("Cannot get rate limits for account ''{}''", accountId);
log.warn(msg, e);
}
} else {
try {
limiter = limitsPerRemoteHost.get(remoteHost).get();
} catch (ExecutionException e) {
String msg = MessageFormat.format(
"Cannot get rate limits for anonymous access from remote host ''{0}''",
remoteHost);
log.warn(msg, e);
}
}
if (limiter != null && !limiter.tryAcquire()) {
throw new RateLimitException(
MessageFormat.format("Exceeded rate limit of {0,number,##.##} fetch requests/hour",
limiter.getRate() * SECONDS_PER_HOUR));
}
}
@Override
public void onPreUpload(Repository repository, Project project,
String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
Collection<? extends ObjectId> haves) throws ValidationException {
}
}