// 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.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 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;
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;

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;
  private final String limitExceededMsg;

  @Inject
  RateLimitUploadListener(
      Provider<CurrentUser> user,
      @Named(Module.CACHE_NAME_ACCOUNTID) LoadingCache<Account.Id, Holder> limitsPerAccount,
      @Named(Module.CACHE_NAME_REMOTEHOST) LoadingCache<String, Holder> limitsPerRemoteHost,
      @Named(RateMsgHelper.UPLOADPACK_CONFIGURABLE_MSG_ANNOTATION) String limitExceededMsg) {
    this.user = user;
    this.limitsPerAccount = limitsPerAccount;
    this.limitsPerRemoteHost = limitsPerRemoteHost;
    this.limitExceededMsg = limitExceededMsg;
  }

  @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(limitExceededMsg, 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 {}
}

@@ -6053,16 +6053,17 @@
 ount ''%7B
+0
 %7D''%22, ac

Fix bug in message formatting