Merge branch 'stable-2.13'
* stable-2.13:
Implement rate limits per account for fetch requests
Change-Id: I06eff5786ba383f15a3f934d34319b0c48e82751
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java
new file mode 100644
index 0000000..7567855
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsConfig.java
@@ -0,0 +1,190 @@
+// 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 com.google.common.collect.ArrayTable;
+import com.google.common.collect.Table;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.ConfigEnum;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AccountLimitsConfig {
+ private static final Logger log =
+ LoggerFactory.getLogger(AccountLimitsConfig.class);
+ static final String GROUP_SECTION = "group";
+ static final SectionParser<AccountLimitsConfig> KEY =
+ new SectionParser<AccountLimitsConfig>() {
+ @Override
+ public AccountLimitsConfig parse(final Config cfg) {
+ return new AccountLimitsConfig(cfg);
+ }
+ };
+
+ public static class RateLimit {
+ public Type getType() {
+ return type;
+ }
+
+ public double getRatePerSecond() {
+ return ratePerSecond;
+ }
+
+ public int getMaxBurstSeconds() {
+ return maxBurstSeconds;
+ }
+
+ private Type type;
+ private double ratePerSecond;
+ private int maxBurstSeconds;
+
+ public RateLimit(Type type, double ratePerSecond, int maxBurstSeconds) {
+ this.type = type;
+ this.ratePerSecond = ratePerSecond;
+ this.maxBurstSeconds = maxBurstSeconds;
+ }
+ }
+
+ public static enum Type implements ConfigEnum {
+ UPLOADPACK;
+
+ @Override
+ public String toConfigValue() {
+ return name().toLowerCase(Locale.ROOT);
+ }
+
+ @Override
+ public boolean matchConfigValue(String in) {
+ return name().equalsIgnoreCase(in);
+ }
+ }
+
+ private Table<Type, String, RateLimit> rateLimits;
+
+ private AccountLimitsConfig(final Config c) {
+ Set<String> groups = c.getSubsections(GROUP_SECTION);
+ if (groups.size() == 0) {
+ return;
+ }
+ rateLimits = ArrayTable.create(Arrays.asList(Type.values()), groups);
+ for (String groupName : groups) {
+ Type type = Type.UPLOADPACK;
+ rateLimits.put(type, groupName,
+ parseRateLimit(c, groupName, type, 60, 30));
+ }
+ }
+
+ RateLimit parseRateLimit(Config c, String groupName, Type type,
+ int defaultIntervalSeconds, int defaultBurstCount) {
+ String name = type.toConfigValue();
+ String value = c.getString(GROUP_SECTION, groupName, name).trim();
+ if (value == null) {
+ return defaultRateLimit(type, defaultIntervalSeconds, defaultBurstCount);
+ }
+
+ Matcher m = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(.*)\\s*burst\\s*(\\d+)$")
+ .matcher(value);
+ if (!m.matches()) {
+ log.warn(
+ "Invalid ''{}'' ratelimit configuration ''{}'', use default ratelimit {}/hour",
+ type.toConfigValue(), value, 3600.0D / defaultIntervalSeconds);
+ return defaultRateLimit(type, defaultIntervalSeconds, defaultBurstCount);
+ }
+
+ String digits = m.group(1);
+ String unitName = m.group(2).trim();
+ String storeCountString = m.group(3).trim();
+ long burstCount = defaultBurstCount;
+ try {
+ burstCount = Long.parseLong(storeCountString);
+ } catch (NumberFormatException e) {
+ log.warn(
+ "Invalid ''{}'' ratelimit store configuration ''{}'', use default burst count ''{}''",
+ type.toConfigValue(), storeCountString, burstCount);
+ }
+
+ TimeUnit inputUnit = TimeUnit.HOURS;
+ double ratePerSecond = 1.0D / defaultIntervalSeconds;
+ if (unitName.isEmpty()) {
+ inputUnit = TimeUnit.SECONDS;
+ } else if (match(unitName, "s", "sec", "second")) {
+ inputUnit = TimeUnit.SECONDS;
+ } else if (match(unitName, "m", "min", "minute")) {
+ inputUnit = TimeUnit.MINUTES;
+ } else if (match(unitName, "h", "hr", "hour")) {
+ inputUnit = TimeUnit.HOURS;
+ } else if (match(unitName, "d", "day")) {
+ inputUnit = TimeUnit.DAYS;
+ } else {
+ logNotRateUnit(GROUP_SECTION, groupName, name, value);
+ }
+ try {
+ ratePerSecond = 1.0D * Long.parseLong(digits)
+ / TimeUnit.SECONDS.convert(1, inputUnit);
+ } catch (NumberFormatException nfe) {
+ logNotRateUnit(GROUP_SECTION, groupName, unitName, value);
+ }
+
+ int maxBurstSeconds = (int) (burstCount / ratePerSecond);
+ return new RateLimit(type, ratePerSecond, maxBurstSeconds);
+ }
+
+ private static boolean match(final String a, final String... cases) {
+ for (final String b : cases) {
+ if (b != null && b.equalsIgnoreCase(a)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void logNotRateUnit(String section, String subsection, String name,
+ String valueString) {
+ if (subsection != null) {
+ log.error(MessageFormat.format("Invalid rate unit value: {0}.{1}.{2}={3}",
+ section, subsection, name, valueString));
+ } else {
+ log.error(MessageFormat.format("Invalid rate unit value: {0}.{1}={2}",
+ section, name, valueString));
+ }
+ }
+
+ private RateLimit defaultRateLimit(Type type, int defaultIntervalSeconds,
+ int defaultStoreCount) {
+ return new RateLimit(type, 1.0D / defaultIntervalSeconds,
+ defaultIntervalSeconds * defaultStoreCount);
+ }
+
+ /**
+ * @param type type of rate limit
+ * @return map of rate limits per group name
+ */
+ Optional<Map<String, RateLimit>> getRatelimits(Type type) {
+ if (rateLimits != null) {
+ return Optional.ofNullable(rateLimits.row(type));
+ }
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java
new file mode 100644
index 0000000..751735c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/AccountLimitsFinder.java
@@ -0,0 +1,95 @@
+// 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.AccountLimitsConfig.KEY;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.RateLimit;
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.Type;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class AccountLimitsFinder {
+ private static final Logger log =
+ LoggerFactory.getLogger(AccountLimitsFinder.class);
+
+ private final ProjectCache projectCache;
+ private final GroupsCollection groupsCollection;
+
+ @Inject
+ AccountLimitsFinder(ProjectCache projectCache,
+ GroupsCollection groupsCollection) {
+ this.projectCache = projectCache;
+ this.groupsCollection = groupsCollection;
+ }
+
+ /**
+ * @param type type of rate limit
+ * @param user identified user
+ * @return the rate limit matching the first configured group limit the given
+ * user is a member of
+ */
+ public Optional<RateLimit> firstMatching(AccountLimitsConfig.Type type,
+ IdentifiedUser user) {
+ Optional<Map<String, AccountLimitsConfig.RateLimit>> limits =
+ getRatelimits(type);
+ if (limits.isPresent()) {
+ GroupMembership memberShip = user.getEffectiveGroups();
+ for (String groupName : limits.get().keySet()) {
+ GroupDescription.Basic d = groupsCollection.parseId(groupName);
+ if (d == null) {
+ log.error("Ignoring limits for unknown group ''{}'' in quota.config",
+ groupName);
+ } else if (memberShip.contains(d.getGroupUUID())) {
+ return Optional.ofNullable(limits.get().get(groupName));
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * @param type type of rate limit
+ * @param groupName name of group to lookup up rate limit for
+ * @return rate limit
+ */
+ public Optional<RateLimit> getRateLimit(Type type, String groupName) {
+ if (getRatelimits(type).isPresent()) {
+ return Optional.ofNullable(getRatelimits(type).get().get(groupName));
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * @param type type of rate limit
+ * @return map of rate limits per group name
+ */
+ private Optional<Map<String, RateLimit>> getRatelimits(Type type) {
+ Config cfg = projectCache.getAllProjects().getConfig("quota.config").get();
+ AccountLimitsConfig limitsCfg = cfg.get(KEY);
+ return limitsCfg.getRatelimits(type);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
index 6814967..935498a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
@@ -15,24 +15,36 @@
package com.googlesource.gerrit.plugins.quota;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
import static com.googlesource.gerrit.plugins.quota.QuotaResource.QUOTA_KIND;
+import com.google.common.cache.CacheLoader;
+import com.google.common.util.concurrent.RateLimiter;
import com.google.gerrit.extensions.events.GarbageCollectorListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
-import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Scopes;
import com.google.inject.internal.UniqueAnnotations;
-
+import com.googlesource.gerrit.plugins.quota.AccountLimitsConfig.RateLimit;
+import java.util.Optional;
import org.eclipse.jgit.transport.PostReceiveHook;
-class Module extends AbstractModule {
+class Module extends CacheModule {
+ static final String CACHE_NAME_ACCOUNTID = "rate_limits_by_account";
+ static final String CACHE_NAME_REMOTEHOST = "rate_limits_by_ip";
@Override
protected void configure() {
@@ -62,5 +74,72 @@
bind(LifecycleListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(PublisherScheduler.class);
+
+ DynamicSet.bind(binder(), UploadValidationListener.class)
+ .to(RateLimitUploadListener.class);
+ cache(CACHE_NAME_ACCOUNTID, Account.Id.class, Holder.class)
+ .loader(LoaderAccountId.class);
+ cache(CACHE_NAME_REMOTEHOST, String.class, Holder.class)
+ .loader(LoaderRemoteHost.class);
+ }
+
+ static class Holder {
+ public static final Holder EMPTY = new Holder(null);
+ private RateLimiter l;
+
+ public Holder(RateLimiter l) {
+ this.l = l;
+ }
+
+ public RateLimiter get() {
+ return l;
+ }
+ }
+
+ private static class LoaderAccountId extends CacheLoader<Account.Id, Holder> {
+ private GenericFactory userFactory;
+ private AccountLimitsFinder finder;
+
+ @Inject
+ LoaderAccountId(IdentifiedUser.GenericFactory userFactory,
+ AccountLimitsFinder finder) {
+ this.userFactory = userFactory;
+ this.finder = finder;
+ }
+
+ @Override
+ public Holder load(Account.Id key) throws Exception {
+ IdentifiedUser user = userFactory.create(key);
+ Optional<RateLimit> limit =
+ finder.firstMatching(AccountLimitsConfig.Type.UPLOADPACK, user);
+ if (limit.isPresent()) {
+ return new Holder(RateLimitUploadListener.createSmoothBurstyRateLimiter(
+ limit.get().getRatePerSecond(), limit.get().getMaxBurstSeconds()));
+ }
+ return Holder.EMPTY;
+ }
+ }
+
+ private static class LoaderRemoteHost extends CacheLoader<String, Holder> {
+ private AccountLimitsFinder finder;
+ private String anonymous;
+
+ @Inject
+ LoaderRemoteHost(SystemGroupBackend systemGroupBackend,
+ AccountLimitsFinder finder) {
+ this.finder = finder;
+ this.anonymous = systemGroupBackend.get(ANONYMOUS_USERS).getName();
+ }
+
+ @Override
+ public Holder load(String key) throws Exception {
+ Optional<RateLimit> limit =
+ finder.getRateLimit(AccountLimitsConfig.Type.UPLOADPACK, anonymous);
+ if (limit.isPresent()) {
+ return new Holder(RateLimitUploadListener.createSmoothBurstyRateLimiter(
+ limit.get().getRatePerSecond(), limit.get().getMaxBurstSeconds()));
+ }
+ return Holder.EMPTY;
+ }
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitException.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitException.java
new file mode 100644
index 0000000..daa6a77
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitException.java
@@ -0,0 +1,25 @@
+// 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 com.google.gerrit.server.validators.ValidationException;
+
+public class RateLimitException extends ValidationException {
+ private static final long serialVersionUID = 1L;
+
+ RateLimitException(String msg) {
+ super(msg);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java
new file mode 100644
index 0000000..64ec2b1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/RateLimitUploadListener.java
@@ -0,0 +1,168 @@
+// 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 {
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 5091958..a20a452 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -2,7 +2,8 @@
To protect a Gerrit installation it makes sense to limit the resources
that a project or group can consume. To do this a Gerrit administrator
-can use this plugin to define quotas on project namespaces.
+can use this plugin to define quotas on project namespaces and define
+rate limits per user group.
The @PLUGIN@ plugin supports the following quotas:
@@ -11,3 +12,10 @@
The measured repository sizes can be published periodically to registered
UsageDataPublishedListeners.
+
+The @PLUGIN@ plugin supports the following rate limits:
+
+* `uploadpack` requests which are executed when a client runs a fetch command.
+
+Rate limits define the maximum request rate for users in a given group
+for a given request type.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 31c32b9..8bf28c1 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -133,6 +133,90 @@
packedObjectsSize*, where *looseObjectsSize* and *packedObjectsSize* are given
by JGit RepoStatistics. By default, false.
+Rate Limits
+-----------
+
+The defined rate limits are stored in a `quota.config` file in the
+`refs/meta/config` branch of the `All-Projects` root project. Rate
+limits are defined per user group and rate limit type:
+
+Example:
+```
+[group "buildserver"]
+ uploadpack = 10 / min burst 500
+
+[group "Registered Users"]
+ uploadpack = 1 /min burst 180
+
+[group "Anonymous Users"]
+ uploadpack = 6/h burst 12
+```
+
+For logged in users rate limits are associated to their accountId. For
+anonymous users rate limits are associated to their remote host address.
+If multiple anonymous users are accessing Gerrit via the same host (e.g.
+a proxy) they share a common rate limit.
+
+If a user is a member of multiple groups mentioned in `quota.config`
+the limit applies that is defined first in the `quota.config` file.
+This resolves ambiguity in case the user is a member of multiple groups
+used in the configuration.
+
+Use group "Anonymous Users" to define the rate limit for anonymous users.
+Use group "Registered Users" to define the default rate limit for all logged
+in users.
+
+Format of the rate limit entries in `quota.config`:
+```
+[group "<groupName>"]
+ <rateLimitType> = <rateLimit> <rateUnit> burst <storedRequests>
+```
+
+<a id="rateLimitType>">
+`group.<groupName>.<rateLimitType>`
+: identifies which request type is limited by this configuration.
+The following rate limit types are supported:
+* `uploadpack`: rate limit for uploadpack (fetch) requests
+The group can be defined by its name or UUID.
+
+<a id="uploadpack">
+`group.<groupName>.uploadpack`
+: rate limit for uploadpack (fetch) requests for the given group. The
+group can be defined by its name or UUID.
+
+<a id="rateLimit">
+: The rate limit (first parameter) defines the maximum allowed request rate.
+
+<a id="rateUnit">
+: Rate limits can be defined using the following rate units:
+`/s`, `/sec`, `/second`: requests per second
+`/m`, `/min`, `/minute`: requests per minute
+`/h`, `/hr`, `/hour`: requests per hour
+`/d`, `/day`: requests per day
+
+The default unit used if no unit is configured is `/hour`.
+
+<a id="burst">
+The `burst` parameter allows to define how many unused requests can be
+stored for later use during idle times. This allows clients to send
+bursts of requests exceeding their rate limit until all their stored
+requests are consumed.
+
+If a rate limit configuration value is invalid a default rate limit of 1
+request per minute with 30 stored requests is assumed.
+
+Example:
+
+Configure a rate limit of maximum 30 fetch request per hour for
+the group of registered users. Up to 60 unused requests can be stored
+during idle times which may be consumed at a later time to send bursts
+of requests above the maximum request rate.
+
+```
+[group "Registered Users"]
+ uploadpack = 30/hour burst 60
+```
+
Publication Schedule
--------------------