Merge "Merge branch 'stable-3.0'"

This commit is contained in:
David Pursehouse
2019-07-27 03:23:36 +00:00
committed by Gerrit Code Review
25 changed files with 560 additions and 28 deletions

View File

@@ -112,7 +112,8 @@ patchSet:: link:json.html#patchSet[patchSet attribute]
submitter:: link:json.html#account[account attribute] submitter:: link:json.html#account[account attribute]
newRev:: The resulting revision of the merge. newRev:: The state (revision) of the target branch after the operation that
closed the change was completed.
eventCreatedOn:: Time in seconds since the UNIX epoch when this event was eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
created. created.

View File

@@ -4271,7 +4271,8 @@ Number of threads to use when executing SSH command requests.
If additional requests are received while all threads are busy they If additional requests are received while all threads are busy they
are queued and serviced in a first-come-first-served order. are queued and serviced in a first-come-first-served order.
+ +
By default, 2x the number of CPUs available to the JVM. By default, 2x the number of CPUs available to the JVM (but at least 4
threads).
+ +
[NOTE] [NOTE]
When SSH daemon is enabled then this setting also defines the max number of When SSH daemon is enabled then this setting also defines the max number of

View File

@@ -1072,18 +1072,18 @@ maven_jar(
sha1 = "0f5a654e4675769c716e5b387830d19b501ca191", sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
) )
TESTCONTAINERS_VERSION = "1.11.4" TESTCONTAINERS_VERSION = "1.12.0"
maven_jar( maven_jar(
name = "testcontainers", name = "testcontainers",
artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION, artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
sha1 = "b0c70b1a3608f43deafba7649b344a422a442585", sha1 = "ac89643ce1ddde504da09172086aba0c7df10bff",
) )
maven_jar( maven_jar(
name = "testcontainers-elasticsearch", name = "testcontainers-elasticsearch",
artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION, artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
sha1 = "faab09a8876b8dbb326cbc10bbaa5ea86f5f5299", sha1 = "cd9020f1803396c45ef935312bf232f9b17332b0",
) )
maven_jar( maven_jar(

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.acceptance;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
@@ -109,7 +110,8 @@ public class EventRecorder {
return events; return events;
} }
private ImmutableList<ChangeMergedEvent> getChangeMergedEvents( @VisibleForTesting
public ImmutableList<ChangeMergedEvent> getChangeMergedEvents(
String project, String branch, int expectedSize) { String project, String branch, int expectedSize) {
String key = refEventKey(ChangeMergedEvent.TYPE, project, branch); String key = refEventKey(ChangeMergedEvent.TYPE, project, branch);
if (expectedSize == 0) { if (expectedSize == 0) {

View File

@@ -14,6 +14,10 @@
package com.google.gerrit.acceptance; package com.google.gerrit.acceptance;
import static com.google.gerrit.server.git.receive.LazyPostReceiveHookChain.affectsSize;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.InProcessProtocol.Context; import com.google.gerrit.acceptance.InProcessProtocol.Context;
import com.google.gerrit.common.data.Capable; import com.google.gerrit.common.data.Capable;
@@ -40,6 +44,9 @@ import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaException;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -261,6 +268,7 @@ class InProcessProtocol extends TestProtocol<Context> {
private final DynamicSet<PostReceiveHook> postReceiveHooks; private final DynamicSet<PostReceiveHook> postReceiveHooks;
private final ThreadLocalRequestContext threadContext; private final ThreadLocalRequestContext threadContext;
private final PermissionBackend permissionBackend; private final PermissionBackend permissionBackend;
private final QuotaBackend quotaBackend;
@Inject @Inject
Receive( Receive(
@@ -271,7 +279,8 @@ class InProcessProtocol extends TestProtocol<Context> {
PluginSetContext<ReceivePackInitializer> receivePackInitializers, PluginSetContext<ReceivePackInitializer> receivePackInitializers,
DynamicSet<PostReceiveHook> postReceiveHooks, DynamicSet<PostReceiveHook> postReceiveHooks,
ThreadLocalRequestContext threadContext, ThreadLocalRequestContext threadContext,
PermissionBackend permissionBackend) { PermissionBackend permissionBackend,
QuotaBackend quotaBackend) {
this.userProvider = userProvider; this.userProvider = userProvider;
this.projectCache = projectCache; this.projectCache = projectCache;
this.factory = factory; this.factory = factory;
@@ -280,6 +289,7 @@ class InProcessProtocol extends TestProtocol<Context> {
this.postReceiveHooks = postReceiveHooks; this.postReceiveHooks = postReceiveHooks;
this.threadContext = threadContext; this.threadContext = threadContext;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.quotaBackend = quotaBackend;
} }
@Override @Override
@@ -319,10 +329,35 @@ class InProcessProtocol extends TestProtocol<Context> {
receivePackInitializers.runEach( receivePackInitializers.runEach(
initializer -> initializer.init(projectState.getNameKey(), rp)); initializer -> initializer.init(projectState.getNameKey(), rp));
QuotaResponse.Aggregated availableTokens =
quotaBackend
.user(identifiedUser)
.project(req.project)
.availableTokens(REPOSITORY_SIZE_GROUP);
availableTokens.throwOnError();
availableTokens.availableTokens().ifPresent(v -> rp.setMaxObjectSizeLimit(v));
rp.setPostReceiveHook(PostReceiveHookChain.newChain(Lists.newArrayList(postReceiveHooks))); ImmutableList<PostReceiveHook> hooks =
ImmutableList.<PostReceiveHook>builder()
.add(
(pack, commands) -> {
if (affectsSize(pack, commands)) {
try {
quotaBackend
.user(identifiedUser)
.project(req.project)
.requestTokens(REPOSITORY_SIZE_GROUP, pack.getPackSize())
.throwOnError();
} catch (QuotaException e) {
throw new RuntimeException(e);
}
}
})
.addAll(postReceiveHooks)
.build();
rp.setPostReceiveHook(PostReceiveHookChain.newChain(hooks));
return rp; return rp;
} catch (IOException | PermissionBackendException e) { } catch (IOException | PermissionBackendException | QuotaException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View File

@@ -98,7 +98,7 @@ public class AccountCacheImpl implements AccountCache {
return byId.get(accountId); return byId.get(accountId);
} catch (ExecutionException e) { } catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot load AccountState for ID %s", accountId); logger.atWarning().withCause(e).log("Cannot load AccountState for ID %s", accountId);
return null; return Optional.empty();
} }
} }
@@ -146,7 +146,7 @@ public class AccountCacheImpl implements AccountCache {
.orElseGet(Optional::empty); .orElseGet(Optional::empty);
} catch (IOException | ConfigInvalidException e) { } catch (IOException | ConfigInvalidException e) {
logger.atWarning().withCause(e).log("Cannot load AccountState for username %s", username); logger.atWarning().withCause(e).log("Cannot load AccountState for username %s", username);
return null; return Optional.empty();
} }
} }

View File

@@ -59,7 +59,6 @@ public class NotifyResolver {
public abstract NotifyHandling handling(); public abstract NotifyHandling handling();
// TODO(dborowitz): Should be ImmutableSetMultimap.
public abstract ImmutableSetMultimap<RecipientType, Account.Id> accounts(); public abstract ImmutableSetMultimap<RecipientType, Account.Id> accounts();
public Result withHandling(NotifyHandling notifyHandling) { public Result withHandling(NotifyHandling notifyHandling) {

View File

@@ -28,7 +28,7 @@ public class ThreadSettingsConfig {
@Inject @Inject
ThreadSettingsConfig(@GerritServerConfig Config cfg) { ThreadSettingsConfig(@GerritServerConfig Config cfg) {
int cores = Runtime.getRuntime().availableProcessors(); int cores = Runtime.getRuntime().availableProcessors();
sshdThreads = cfg.getInt("sshd", "threads", 2 * cores); sshdThreads = cfg.getInt("sshd", "threads", Math.max(4, 2 * cores));
httpdMaxThreads = cfg.getInt("httpd", "maxThreads", 25); httpdMaxThreads = cfg.getInt("httpd", "maxThreads", 25);
int defaultDatabasePoolLimit = sshdThreads + httpdMaxThreads + 2; int defaultDatabasePoolLimit = sshdThreads + httpdMaxThreads + 2;
databasePoolLimit = cfg.getInt("database", "poolLimit", defaultDatabasePoolLimit); databasePoolLimit = cfg.getInt("database", "poolLimit", defaultDatabasePoolLimit);

View File

@@ -49,7 +49,10 @@ public class MergedByPushOp implements BatchUpdateOp {
public interface Factory { public interface Factory {
MergedByPushOp create( MergedByPushOp create(
RequestScopePropagator requestScopePropagator, PatchSet.Id psId, String refName); RequestScopePropagator requestScopePropagator,
PatchSet.Id psId,
@Assisted("refName") String refName,
@Assisted("mergeResultRevId") String mergeResultRevId);
} }
private final RequestScopePropagator requestScopePropagator; private final RequestScopePropagator requestScopePropagator;
@@ -62,6 +65,7 @@ public class MergedByPushOp implements BatchUpdateOp {
private final PatchSet.Id psId; private final PatchSet.Id psId;
private final String refName; private final String refName;
private final String mergeResultRevId;
private Change change; private Change change;
private boolean correctBranch; private boolean correctBranch;
@@ -79,7 +83,8 @@ public class MergedByPushOp implements BatchUpdateOp {
ChangeMerged changeMerged, ChangeMerged changeMerged,
@Assisted RequestScopePropagator requestScopePropagator, @Assisted RequestScopePropagator requestScopePropagator,
@Assisted PatchSet.Id psId, @Assisted PatchSet.Id psId,
@Assisted String refName) { @Assisted("refName") String refName,
@Assisted("mergeResultRevId") String mergeResultRevId) {
this.patchSetInfoFactory = patchSetInfoFactory; this.patchSetInfoFactory = patchSetInfoFactory;
this.cmUtil = cmUtil; this.cmUtil = cmUtil;
this.mergedSenderFactory = mergedSenderFactory; this.mergedSenderFactory = mergedSenderFactory;
@@ -89,6 +94,7 @@ public class MergedByPushOp implements BatchUpdateOp {
this.requestScopePropagator = requestScopePropagator; this.requestScopePropagator = requestScopePropagator;
this.psId = psId; this.psId = psId;
this.refName = refName; this.refName = refName;
this.mergeResultRevId = mergeResultRevId;
} }
public String getMergedIntoRef() { public String getMergedIntoRef() {
@@ -184,8 +190,7 @@ public class MergedByPushOp implements BatchUpdateOp {
} }
})); }));
changeMerged.fire( changeMerged.fire(change, patchSet, ctx.getAccount(), mergeResultRevId, ctx.getWhen());
change, patchSet, ctx.getAccount(), patchSet.commitId().name(), ctx.getWhen());
} }
private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException { private PatchSetInfo getPatchSetInfo(ChangeContext ctx) throws IOException {

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.git.receive; package com.google.gerrit.server.git.receive;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import static java.util.concurrent.TimeUnit.NANOSECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
@@ -45,6 +46,9 @@ import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.ContributorAgreementsChecker;
import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaException;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.inject.Inject; import com.google.inject.Inject;
@@ -92,6 +96,7 @@ public class AsyncReceiveCommits implements PreReceiveHook {
public static class Module extends PrivateModule { public static class Module extends PrivateModule {
@Override @Override
public void configure() { public void configure() {
install(new FactoryModuleBuilder().build(LazyPostReceiveHookChain.Factory.class));
install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class)); install(new FactoryModuleBuilder().build(AsyncReceiveCommits.Factory.class));
expose(AsyncReceiveCommits.Factory.class); expose(AsyncReceiveCommits.Factory.class);
// Don't expose the binding for ReceiveCommits.Factory. All callers should // Don't expose the binding for ReceiveCommits.Factory. All callers should
@@ -257,9 +262,10 @@ public class AsyncReceiveCommits implements PreReceiveHook {
RequestScopePropagator scopePropagator, RequestScopePropagator scopePropagator,
ReceiveConfig receiveConfig, ReceiveConfig receiveConfig,
TransferConfig transferConfig, TransferConfig transferConfig,
Provider<LazyPostReceiveHookChain> lazyPostReceive, LazyPostReceiveHookChain.Factory lazyPostReceive,
ContributorAgreementsChecker contributorAgreements, ContributorAgreementsChecker contributorAgreements,
Metrics metrics, Metrics metrics,
QuotaBackend quotaBackend,
@Named(TIMEOUT_NAME) long timeoutMillis, @Named(TIMEOUT_NAME) long timeoutMillis,
@Assisted ProjectState projectState, @Assisted ProjectState projectState,
@Assisted IdentifiedUser user, @Assisted IdentifiedUser user,
@@ -288,7 +294,7 @@ public class AsyncReceiveCommits implements PreReceiveHook {
receivePack.setRefFilter(new ReceiveRefFilter()); receivePack.setRefFilter(new ReceiveRefFilter());
receivePack.setAllowPushOptions(true); receivePack.setAllowPushOptions(true);
receivePack.setPreReceiveHook(this); receivePack.setPreReceiveHook(this);
receivePack.setPostReceiveHook(lazyPostReceive.get()); receivePack.setPostReceiveHook(lazyPostReceive.create(user, projectName));
// If the user lacks READ permission, some references may be filtered and hidden from view. // If the user lacks READ permission, some references may be filtered and hidden from view.
// Check objects mentioned inside the incoming pack file are reachable from visible refs. // Check objects mentioned inside the incoming pack file are reachable from visible refs.
@@ -310,6 +316,17 @@ public class AsyncReceiveCommits implements PreReceiveHook {
factory.create( factory.create(
projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds); projectState, user, receivePack, allRefsWatcher, messageSender, resultChangeIds);
receiveCommits.init(); receiveCommits.init();
QuotaResponse.Aggregated availableTokens =
quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
try {
availableTokens.throwOnError();
} catch (QuotaException e) {
logger.atWarning().withCause(e).log(
"Quota %s availableTokens request failed for project %s",
REPOSITORY_SIZE_GROUP, projectName);
throw new RuntimeException(e);
}
availableTokens.availableTokens().ifPresent(v -> receivePack.setMaxObjectSizeLimit(v));
} }
/** Determine if the user can upload commits. */ /** Determine if the user can upload commits. */

View File

@@ -14,23 +14,78 @@
package com.google.gerrit.server.git.receive; package com.google.gerrit.server.git.receive;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.Collection; import java.util.Collection;
import org.eclipse.jgit.transport.PostReceiveHook; import org.eclipse.jgit.transport.PostReceiveHook;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ReceivePack;
class LazyPostReceiveHookChain implements PostReceiveHook { /**
* Class is responsible for calling all registered post-receive hooks. In addition, in case when
* repository size quota is defined, it requests tokens (pack size) that were received. This is the
* final step of enforcing repository size quota that deducts token from available tokens.
*/
public class LazyPostReceiveHookChain implements PostReceiveHook {
interface Factory {
LazyPostReceiveHookChain create(CurrentUser user, Project.NameKey project);
}
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PluginSetContext<PostReceiveHook> hooks; private final PluginSetContext<PostReceiveHook> hooks;
private final QuotaBackend quotaBackend;
private final CurrentUser user;
private final Project.NameKey project;
@Inject @Inject
LazyPostReceiveHookChain(PluginSetContext<PostReceiveHook> hooks) { LazyPostReceiveHookChain(
PluginSetContext<PostReceiveHook> hooks,
QuotaBackend quotaBackend,
@Assisted CurrentUser user,
@Assisted Project.NameKey project) {
this.hooks = hooks; this.hooks = hooks;
this.quotaBackend = quotaBackend;
this.user = user;
this.project = project;
} }
@Override @Override
public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
hooks.runEach(h -> h.onPostReceive(rp, commands)); hooks.runEach(h -> h.onPostReceive(rp, commands));
if (affectsSize(rp, commands)) {
QuotaResponse.Aggregated a =
quotaBackend
.user(user)
.project(project)
.requestTokens(REPOSITORY_SIZE_GROUP, rp.getPackSize());
if (a.hasError()) {
String msg =
String.format(
"%s request failed for project %s with [%s]",
REPOSITORY_SIZE_GROUP, project, a.errorMessage());
logger.atWarning().log(msg);
throw new RuntimeException(msg);
}
}
}
public static boolean affectsSize(ReceivePack rp, Collection<ReceiveCommand> commands) {
if (rp.getPackSize() > 0L) {
for (ReceiveCommand cmd : commands) {
if (cmd.getType() != ReceiveCommand.Type.DELETE) {
return true;
}
}
}
return false;
} }
} }

View File

@@ -2952,6 +2952,7 @@ class ReceiveCommits {
projectState, projectState,
notes.getChange().getDest(), notes.getChange().getDest(),
checkMergedInto, checkMergedInto,
checkMergedInto ? inputCommand.getNewId().name() : null,
priorPatchSet, priorPatchSet,
priorCommit, priorCommit,
psId, psId,
@@ -3187,6 +3188,9 @@ class ReceiveCommits {
int limit = receiveConfig.maxBatchCommits; int limit = receiveConfig.maxBatchCommits;
int n = 0; int n = 0;
for (RevCommit c; (c = walk.next()) != null; ) { for (RevCommit c; (c = walk.next()) != null; ) {
// Even if skipValidation is set, we still get here when at least one plugin
// commit validator requires to validate all commits. In this case, however,
// we don't need to check the commit limit.
if (++n > limit && !skipValidation) { if (++n > limit && !skipValidation) {
logger.atFine().log("Number of new commits exceeds limit of %d", limit); logger.atFine().log("Number of new commits exceeds limit of %d", limit);
reject( reject(
@@ -3263,7 +3267,8 @@ class ReceiveCommits {
bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null)); bu.addOp(notes.get().getChangeId(), setPrivateOpFactory.create(false, null));
bu.addOp( bu.addOp(
psId.changeId(), psId.changeId(),
mergedByPushOpFactory.create(requestScopePropagator, psId, refName)); mergedByPushOpFactory.create(
requestScopePropagator, psId, refName, newTip.getId().getName()));
continue COMMIT; continue COMMIT;
} }
} }
@@ -3297,7 +3302,8 @@ class ReceiveCommits {
bu.addOp( bu.addOp(
id, id,
mergedByPushOpFactory mergedByPushOpFactory
.create(requestScopePropagator, req.psId, refName) .create(
requestScopePropagator, req.psId, refName, newTip.getId().getName())
.setPatchSetProvider(req.replaceOp::getPatchSet)); .setPatchSetProvider(req.replaceOp::getPatchSet));
bu.addOp(id, new ChangeProgressOp(progress)); bu.addOp(id, new ChangeProgressOp(progress));
ids.add(id); ids.add(id);

View File

@@ -101,6 +101,7 @@ public class ReplaceOp implements BatchUpdateOp {
ProjectState projectState, ProjectState projectState,
BranchNameKey dest, BranchNameKey dest,
boolean checkMergedInto, boolean checkMergedInto,
@Nullable String mergeResultRevId,
@Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId, @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@Assisted("priorCommitId") ObjectId priorCommit, @Assisted("priorCommitId") ObjectId priorCommit,
@Assisted("patchSetId") PatchSet.Id patchSetId, @Assisted("patchSetId") PatchSet.Id patchSetId,
@@ -133,6 +134,7 @@ public class ReplaceOp implements BatchUpdateOp {
private final ProjectState projectState; private final ProjectState projectState;
private final BranchNameKey dest; private final BranchNameKey dest;
private final boolean checkMergedInto; private final boolean checkMergedInto;
private final String mergeResultRevId;
private final PatchSet.Id priorPatchSetId; private final PatchSet.Id priorPatchSetId;
private final ObjectId priorCommitId; private final ObjectId priorCommitId;
private final PatchSet.Id patchSetId; private final PatchSet.Id patchSetId;
@@ -177,6 +179,7 @@ public class ReplaceOp implements BatchUpdateOp {
@Assisted ProjectState projectState, @Assisted ProjectState projectState,
@Assisted BranchNameKey dest, @Assisted BranchNameKey dest,
@Assisted boolean checkMergedInto, @Assisted boolean checkMergedInto,
@Assisted @Nullable String mergeResultRevId,
@Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId, @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
@Assisted("priorCommitId") ObjectId priorCommitId, @Assisted("priorCommitId") ObjectId priorCommitId,
@Assisted("patchSetId") PatchSet.Id patchSetId, @Assisted("patchSetId") PatchSet.Id patchSetId,
@@ -205,6 +208,7 @@ public class ReplaceOp implements BatchUpdateOp {
this.projectState = projectState; this.projectState = projectState;
this.dest = dest; this.dest = dest;
this.checkMergedInto = checkMergedInto; this.checkMergedInto = checkMergedInto;
this.mergeResultRevId = mergeResultRevId;
this.priorPatchSetId = priorPatchSetId; this.priorPatchSetId = priorPatchSetId;
this.priorCommitId = priorCommitId.copy(); this.priorCommitId = priorCommitId.copy();
this.patchSetId = patchSetId; this.patchSetId = patchSetId;
@@ -231,7 +235,8 @@ public class ReplaceOp implements BatchUpdateOp {
String mergedInto = findMergedInto(ctx, dest.branch(), commit); String mergedInto = findMergedInto(ctx, dest.branch(), commit);
if (mergedInto != null) { if (mergedInto != null) {
mergedByPushOp = mergedByPushOp =
mergedByPushOpFactory.create(requestScopePropagator, patchSetId, mergedInto); mergedByPushOpFactory.create(
requestScopePropagator, patchSetId, mergedInto, mergeResultRevId);
} }
} }

View File

@@ -24,6 +24,7 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.plugincontext.PluginSetContext; import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.plugincontext.PluginSetEntryContext; import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
import com.google.gerrit.server.quota.QuotaResponse.Aggregated;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
@@ -100,6 +101,20 @@ public class DefaultQuotaBackend implements QuotaBackend {
return QuotaResponse.Aggregated.create(ImmutableList.copyOf(responses)); return QuotaResponse.Aggregated.create(ImmutableList.copyOf(responses));
} }
private static QuotaResponse.Aggregated availableTokens(
PluginSetContext<QuotaEnforcer> quotaEnforcers,
String quotaGroup,
QuotaRequestContext requestContext) {
// PluginSets can change their content when plugins (de-)register. Copy the currently registered
// plugins so that we can iterate twice on a stable list.
List<PluginSetEntryContext<QuotaEnforcer>> enforcers = ImmutableList.copyOf(quotaEnforcers);
List<QuotaResponse> responses = new ArrayList<>(enforcers.size());
for (PluginSetEntryContext<QuotaEnforcer> enforcer : enforcers) {
responses.add(enforcer.call(p -> p.availableTokens(quotaGroup, requestContext)));
}
return QuotaResponse.Aggregated.create(responses);
}
private static void refillAfterErrorOrException( private static void refillAfterErrorOrException(
List<PluginSetEntryContext<QuotaEnforcer>> enforcers, List<PluginSetEntryContext<QuotaEnforcer>> enforcers,
List<QuotaResponse> collectedResponses, List<QuotaResponse> collectedResponses,
@@ -158,5 +173,10 @@ public class DefaultQuotaBackend implements QuotaBackend {
return DefaultQuotaBackend.request( return DefaultQuotaBackend.request(
quotaEnforcers, quotaGroup, requestContext, numTokens, false); quotaEnforcers, quotaGroup, requestContext, numTokens, false);
} }
@Override
public Aggregated availableTokens(String quotaGroup) {
return DefaultQuotaBackend.availableTokens(quotaEnforcers, quotaGroup, requestContext);
}
} }
} }

View File

@@ -82,5 +82,22 @@ public interface QuotaBackend {
* not to deduct any quota yet. Can be used to do pre-flight requests where necessary * not to deduct any quota yet. Can be used to do pre-flight requests where necessary
*/ */
QuotaResponse.Aggregated dryRun(String quotaGroup, long tokens); QuotaResponse.Aggregated dryRun(String quotaGroup, long tokens);
/**
* Requests a minimum number of tokens available in implementations. This is a pre-flight check
* for the exceptional case when the requested number of tokens is not known in advance but
* boundary can be specified. For instance, when the commit is received its size is not known
* until the transfer happens however one can specify how many bytes can be accepted to meet the
* repository size quota.
*
* <p>By definition, this is not an allocating request, therefore, it should be followed by the
* call to {@link #requestTokens(String, long)} when the size gets determined so that quota
* could be properly adjusted. It is in developer discretion to ensure that it gets called.
* There might be a case when particular quota gets temporarily overbooked when multiple
* requests are performed but the following calls to {@link #requestTokens(String, long)} will
* fail at the moment when a quota is exhausted. It is not a subject of quota backend to reclaim
* tokens that were used due to overbooking.
*/
QuotaResponse.Aggregated availableTokens(String quotaGroup);
} }
} }

View File

@@ -59,6 +59,19 @@ public interface QuotaEnforcer {
*/ */
QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens); QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens);
/**
* Returns available tokens that can be later requested.
*
* <p>This is used as a pre-flight check for the exceptional case when the requested number of
* tokens is not known in advance. Implementation should not deduct tokens from a bucket. It
* should be followed by a call to {@link #requestTokens(String, QuotaRequestContext, long)} with
* the number of tokens that were eventually used. It is in {@link QuotaBackend} callers
* discretion to ensure that {@link
* com.google.gerrit.server.quota.QuotaBackend.WithResource#requestTokens(String, long)} is
* called.
*/
QuotaResponse availableTokens(String quotaGroup, QuotaRequestContext ctx);
/** /**
* A previously requested and deducted quota has to be refilled (if possible) because the request * A previously requested and deducted quota has to be refilled (if possible) because the request
* failed other quota checks. Implementations can choose to leave this a no-op in case they are * failed other quota checks. Implementations can choose to leave this a no-op in case they are

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2019 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.google.gerrit.server.quota;
public class QuotaGroupDefinitions {
/**
* Definition of repository size quota group. {@link QuotaEnforcer} implementations for repository
* size quota have to act on requests with this group name.
*/
public static final String REPOSITORY_SIZE_GROUP = "/repository:size";
private QuotaGroupDefinitions() {}
}

View File

@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@AutoValue @AutoValue
@@ -55,6 +56,10 @@ public abstract class QuotaResponse {
return new AutoValue_QuotaResponse.Builder().status(Status.OK).build(); return new AutoValue_QuotaResponse.Builder().status(Status.OK).build();
} }
public static QuotaResponse ok(long tokens) {
return new AutoValue_QuotaResponse.Builder().status(Status.OK).availableTokens(tokens).build();
}
public static QuotaResponse noOp() { public static QuotaResponse noOp() {
return new AutoValue_QuotaResponse.Builder().status(Status.NO_OP).build(); return new AutoValue_QuotaResponse.Builder().status(Status.NO_OP).build();
} }
@@ -65,12 +70,16 @@ public abstract class QuotaResponse {
public abstract Status status(); public abstract Status status();
public abstract Optional<Long> availableTokens();
public abstract Optional<String> message(); public abstract Optional<String> message();
@AutoValue.Builder @AutoValue.Builder
public abstract static class Builder { public abstract static class Builder {
public abstract QuotaResponse.Builder status(Status status); public abstract QuotaResponse.Builder status(Status status);
public abstract QuotaResponse.Builder availableTokens(Long tokens);
public abstract QuotaResponse.Builder message(String message); public abstract QuotaResponse.Builder message(String message);
public abstract QuotaResponse build(); public abstract QuotaResponse build();
@@ -96,6 +105,13 @@ public abstract class QuotaResponse {
return responses().stream().filter(r -> r.status().isOk()).collect(toImmutableList()); return responses().stream().filter(r -> r.status().isOk()).collect(toImmutableList());
} }
public OptionalLong availableTokens() {
return responses().stream()
.filter(r -> r.status().isOk() && r.availableTokens().isPresent())
.mapToLong(r -> r.availableTokens().get())
.min();
}
public ImmutableList<QuotaResponse> error() { public ImmutableList<QuotaResponse> error() {
return responses().stream().filter(r -> r.status().isError()).collect(toImmutableList()); return responses().stream().filter(r -> r.status().isError()).collect(toImmutableList());
} }

View File

@@ -91,7 +91,7 @@ public class LsUserRefs extends SshCommand {
try { try {
Map<String, Ref> refsMap = Map<String, Ref> refsMap =
permissionBackend permissionBackend
.user(user) .user(ctx.getUser())
.project(projectName) .project(projectName)
.filter(repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults()); .filter(repo.getRefDatabase().getRefs(), repo, RefFilterOptions.defaults());

View File

@@ -2250,6 +2250,34 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
@GerritConfig(name = "receive.maxBatchCommits", value = "2") @GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test @Test
public void maxBatchCommits() throws Exception { public void maxBatchCommits() throws Exception {
testMaxBatchCommits();
}
@GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test
public void maxBatchCommitsWithDefaultValidator() throws Exception {
TestValidator validator = new TestValidator();
RegistrationHandle handle = commitValidators.add("test-validator", validator);
try {
testMaxBatchCommits();
} finally {
handle.remove();
}
}
@GerritConfig(name = "receive.maxBatchCommits", value = "2")
@Test
public void maxBatchCommitsWithValidateAllCommitsValidator() throws Exception {
TestValidator validator = new TestValidator(true);
RegistrationHandle handle = commitValidators.add("test-validator", validator);
try {
testMaxBatchCommits();
} finally {
handle.remove();
}
}
private void testMaxBatchCommits() throws Exception {
List<RevCommit> commits = new ArrayList<>(); List<RevCommit> commits = new ArrayList<>();
commits.addAll(initChanges(2)); commits.addAll(initChanges(2));
String master = "refs/heads/master"; String master = "refs/heads/master";

View File

@@ -38,10 +38,12 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.events.ChangeMergedEvent;
import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender.Message; import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject; import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.api.errors.TransportException;
@@ -201,6 +203,23 @@ public class SubmitOnPushIT extends AbstractDaemonTest {
assertThat(cd.patchSet(psId).commitId()).isEqualTo(c); assertThat(cd.patchSet(psId).commitId()).isEqualTo(c);
} }
@Test
public void correctNewRevOnMergeByPushToBranch() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
.update();
push("refs/for/master", PushOneCommit.SUBJECT, "one.txt", "One");
PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "two.txt", "Two");
startEventRecorder();
git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
List<ChangeMergedEvent> changeMergedEvents =
eventRecorder.getChangeMergedEvents(project.get(), "refs/heads/master", 2);
assertThat(changeMergedEvents.get(0).newRev).isEqualTo(r.getPatchSet().commitId().name());
assertThat(changeMergedEvents.get(1).newRev).isEqualTo(r.getPatchSet().commitId().name());
}
@Test @Test
public void mergeOnPushToBranchWithChangeMergedInOther() throws Exception { public void mergeOnPushToBranchWithChangeMergedInOther() throws Exception {
enableCreateNewChangeForAllNotInTarget(); enableCreateNewChangeForAllNotInTarget();

View File

@@ -22,6 +22,8 @@ import static org.easymock.EasyMock.resetToStrict;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.extensions.annotations.Exports; import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
@@ -35,7 +37,9 @@ import com.google.inject.Module;
import java.util.Collections; import java.util.Collections;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
public class DefaultQuotaBackendIT extends AbstractDaemonTest { public class DefaultQuotaBackendIT extends AbstractDaemonTest {
@@ -44,6 +48,8 @@ public class DefaultQuotaBackendIT extends AbstractDaemonTest {
private IdentifiedUser identifiedAdmin; private IdentifiedUser identifiedAdmin;
@Inject private QuotaBackend quotaBackend; @Inject private QuotaBackend quotaBackend;
@Rule public ExpectedException exception = ExpectedException.none();
@Override @Override
public Module createModule() { public Module createModule() {
return new FactoryModule() { return new FactoryModule() {
@@ -93,7 +99,7 @@ public class DefaultQuotaBackendIT extends AbstractDaemonTest {
@Test @Test
public void requestTokenForUserAndChange() throws Exception { public void requestTokenForUserAndChange() throws Exception {
Change.Id changeId = createChange().getChange().getId(); Change.Id changeId = retrieveChangeId();
QuotaRequestContext ctx = QuotaRequestContext ctx =
QuotaRequestContext.builder() QuotaRequestContext.builder()
.user(identifiedAdmin) .user(identifiedAdmin)
@@ -125,6 +131,58 @@ public class DefaultQuotaBackendIT extends AbstractDaemonTest {
.isEqualTo(singletonAggregation(QuotaResponse.ok())); .isEqualTo(singletonAggregation(QuotaResponse.ok()));
} }
@Test
public void availableTokensForUserAndAccount() {
QuotaRequestContext ctx =
QuotaRequestContext.builder().user(identifiedAdmin).account(user.id()).build();
QuotaResponse r = QuotaResponse.ok(10L);
expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
replay(quotaEnforcer);
assertThat(quotaBackend.user(identifiedAdmin).account(user.id()).availableTokens("testGroup"))
.isEqualTo(singletonAggregation(r));
}
@Test
public void availableTokensForUserAndProject() {
QuotaRequestContext ctx =
QuotaRequestContext.builder().user(identifiedAdmin).project(project).build();
QuotaResponse r = QuotaResponse.ok(10L);
expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
replay(quotaEnforcer);
assertThat(quotaBackend.user(identifiedAdmin).project(project).availableTokens("testGroup"))
.isEqualTo(singletonAggregation(r));
}
@Test
public void availableTokensForUserAndChange() throws Exception {
Change.Id changeId = retrieveChangeId();
QuotaRequestContext ctx =
QuotaRequestContext.builder()
.user(identifiedAdmin)
.change(changeId)
.project(project)
.build();
QuotaResponse r = QuotaResponse.ok(10L);
expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
replay(quotaEnforcer);
assertThat(
quotaBackend
.user(identifiedAdmin)
.change(changeId, project)
.availableTokens("testGroup"))
.isEqualTo(singletonAggregation(r));
}
@Test
public void availableTokens() {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
QuotaResponse r = QuotaResponse.ok(10L);
expect(quotaEnforcer.availableTokens("testGroup", ctx)).andReturn(r);
replay(quotaEnforcer);
assertThat(quotaBackend.user(identifiedAdmin).availableTokens("testGroup"))
.isEqualTo(singletonAggregation(r));
}
@Test @Test
public void requestTokenError() throws Exception { public void requestTokenError() throws Exception {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build(); QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
@@ -138,6 +196,20 @@ public class DefaultQuotaBackendIT extends AbstractDaemonTest {
assertThat(thrown).hasMessageThat().contains("failed"); assertThat(thrown).hasMessageThat().contains("failed");
} }
@Test
public void availableTokensError() throws Exception {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
expect(quotaEnforcer.availableTokens("testGroup", ctx))
.andReturn(QuotaResponse.error("failed"));
replay(quotaEnforcer);
QuotaResponse.Aggregated result =
quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
assertThat(result).isEqualTo(singletonAggregation(QuotaResponse.error("failed")));
exception.expect(QuotaException.class);
exception.expectMessage("failed");
result.throwOnError();
}
@Test @Test
public void requestTokenPluginThrowsAndRethrows() { public void requestTokenPluginThrowsAndRethrows() {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build(); QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
@@ -148,6 +220,23 @@ public class DefaultQuotaBackendIT extends AbstractDaemonTest {
() -> quotaBackend.user(identifiedAdmin).requestToken("testGroup")); () -> quotaBackend.user(identifiedAdmin).requestToken("testGroup"));
} }
@Test
public void availableTokensPluginThrowsAndRethrows() {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
expect(quotaEnforcer.availableTokens("testGroup", ctx)).andThrow(new NullPointerException());
replay(quotaEnforcer);
exception.expect(NullPointerException.class);
quotaBackend.user(identifiedAdmin).availableTokens("testGroup");
}
private Change.Id retrieveChangeId() throws Exception {
// use REST API so that repository size quota doesn't have to be stubbed
ChangeInfo changeInfo =
gApi.changes().create(new ChangeInput(project.get(), "master", "test")).get();
return Change.id(changeInfo._number);
}
private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) { private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
return QuotaResponse.Aggregated.create(Collections.singleton(response)); return QuotaResponse.Aggregated.create(Collections.singleton(response));
} }

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.server.quota.QuotaRequestContext;
import com.google.gerrit.server.quota.QuotaResponse; import com.google.gerrit.server.quota.QuotaResponse;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Module; import com.google.inject.Module;
import java.util.OptionalLong;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -123,4 +124,34 @@ public class MultipleQuotaPluginsIT extends AbstractDaemonTest {
QuotaResponse.Aggregated.create( QuotaResponse.Aggregated.create(
ImmutableList.of(QuotaResponse.error("fail"), QuotaResponse.noOp()))); ImmutableList.of(QuotaResponse.error("fail"), QuotaResponse.noOp())));
} }
@Test
public void minimumAvailableTokens() {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
expect(quotaEnforcerA.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(20L));
expect(quotaEnforcerB.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(10L));
replay(quotaEnforcerA);
replay(quotaEnforcerB);
OptionalLong tokens =
quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
assertThat(tokens.isPresent()).isTrue();
assertThat(tokens.getAsLong()).isEqualTo(10L);
}
@Test
public void ignoreNoOpForAvailableTokens() {
QuotaRequestContext ctx = QuotaRequestContext.builder().user(identifiedAdmin).build();
expect(quotaEnforcerA.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.noOp());
expect(quotaEnforcerB.availableTokens("testGroup", ctx)).andReturn(QuotaResponse.ok(20L));
replay(quotaEnforcerA);
replay(quotaEnforcerB);
OptionalLong tokens =
quotaBackend.user(identifiedAdmin).availableTokens("testGroup").availableTokens();
assertThat(tokens.isPresent()).isTrue();
assertThat(tokens.getAsLong()).isEqualTo(20L);
}
} }

View File

@@ -0,0 +1,140 @@
// Copyright (C) 2019 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.google.gerrit.acceptance.server.quota;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import static com.google.gerrit.server.quota.QuotaResponse.ok;
import static org.easymock.EasyMock.anyLong;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.resetToStrict;
import static org.easymock.EasyMock.verify;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.quota.QuotaBackend;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.inject.Module;
import java.util.Collections;
import org.easymock.EasyMock;
import org.eclipse.jgit.api.errors.TooLargeObjectInPackException;
import org.eclipse.jgit.api.errors.TransportException;
import org.junit.Before;
import org.junit.Test;
@UseLocalDisk
public class RepositorySizeQuotaIT extends AbstractDaemonTest {
private static final QuotaBackend.WithResource quotaBackendWithResource =
EasyMock.createStrictMock(QuotaBackend.WithResource.class);
private static final QuotaBackend.WithUser quotaBackendWithUser =
EasyMock.createStrictMock(QuotaBackend.WithUser.class);
@Override
public Module createModule() {
return new FactoryModule() {
@Override
public void configure() {
bind(QuotaBackend.class)
.toInstance(
new QuotaBackend() {
@Override
public WithUser currentUser() {
return quotaBackendWithUser;
}
@Override
public WithUser user(CurrentUser user) {
return quotaBackendWithUser;
}
});
}
};
}
@Before
public void setUp() {
resetToStrict(quotaBackendWithResource);
resetToStrict(quotaBackendWithUser);
}
@Test
public void pushWithAvailableTokens() throws Exception {
expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
.andReturn(singletonAggregation(ok(276L)))
.times(2);
expect(quotaBackendWithResource.requestTokens(eq(REPOSITORY_SIZE_GROUP), anyLong()))
.andReturn(singletonAggregation(ok()));
expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
replay(quotaBackendWithResource);
replay(quotaBackendWithUser);
pushCommit();
verify(quotaBackendWithUser);
verify(quotaBackendWithResource);
}
@Test
public void pushWithNotSufficientTokens() throws Exception {
long availableTokens = 1L;
expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
.andReturn(singletonAggregation(ok(availableTokens)))
.anyTimes();
expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
replay(quotaBackendWithResource);
replay(quotaBackendWithUser);
try {
pushCommit();
assertWithMessage("expected TooLargeObjectInPackException").fail();
} catch (TooLargeObjectInPackException e) {
String msg = e.getMessage();
assertThat(msg).contains("Object too large");
assertThat(msg)
.contains(String.format("Max object size limit is %d bytes.", availableTokens));
}
verify(quotaBackendWithUser);
verify(quotaBackendWithResource);
}
@Test
public void errorGettingAvailableTokens() throws Exception {
String msg = "quota error";
expect(quotaBackendWithResource.availableTokens(REPOSITORY_SIZE_GROUP))
.andReturn(singletonAggregation(QuotaResponse.error(msg)))
.anyTimes();
expect(quotaBackendWithUser.project(project)).andReturn(quotaBackendWithResource).anyTimes();
replay(quotaBackendWithResource);
replay(quotaBackendWithUser);
try {
pushCommit();
assertWithMessage("expected TransportException").fail();
} catch (TransportException e) {
// TransportException has not much info about the cause
}
verify(quotaBackendWithUser);
verify(quotaBackendWithResource);
}
private void pushCommit() throws Exception {
createCommitAndPush(testRepo, "refs/heads/master", "test 01", "file.test", "some content");
}
private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
return QuotaResponse.Aggregated.create(Collections.singleton(response));
}
}

View File

@@ -20,6 +20,7 @@ import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify; import static org.easymock.EasyMock.verify;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -68,7 +69,7 @@ public class RestApiQuotaIT extends AbstractDaemonTest {
@Test @Test
public void changeDetail() throws Exception { public void changeDetail() throws Exception {
Change.Id changeId = createChange().getChange().getId(); Change.Id changeId = retrieveChangeId();
expect(quotaBackendWithResource.requestToken("/restapi/changes/detail:GET")) expect(quotaBackendWithResource.requestToken("/restapi/changes/detail:GET"))
.andReturn(singletonAggregation(QuotaResponse.ok())); .andReturn(singletonAggregation(QuotaResponse.ok()));
replay(quotaBackendWithResource); replay(quotaBackendWithResource);
@@ -81,7 +82,7 @@ public class RestApiQuotaIT extends AbstractDaemonTest {
@Test @Test
public void revisionDetail() throws Exception { public void revisionDetail() throws Exception {
Change.Id changeId = createChange().getChange().getId(); Change.Id changeId = retrieveChangeId();
expect(quotaBackendWithResource.requestToken("/restapi/changes/revisions/actions:GET")) expect(quotaBackendWithResource.requestToken("/restapi/changes/revisions/actions:GET"))
.andReturn(singletonAggregation(QuotaResponse.ok())); .andReturn(singletonAggregation(QuotaResponse.ok()));
replay(quotaBackendWithResource); replay(quotaBackendWithResource);
@@ -130,6 +131,13 @@ public class RestApiQuotaIT extends AbstractDaemonTest {
adminRestSession.get("/config/server/version").assertStatus(429); adminRestSession.get("/config/server/version").assertStatus(429);
} }
private Change.Id retrieveChangeId() throws Exception {
// use REST API so that repository size quota doesn't have to be stubbed
ChangeInfo changeInfo =
gApi.changes().create(new ChangeInput(project.get(), "master", "test")).get();
return Change.id(changeInfo._number);
}
private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) { private static QuotaResponse.Aggregated singletonAggregation(QuotaResponse response) {
return QuotaResponse.Aggregated.create(Collections.singleton(response)); return QuotaResponse.Aggregated.create(Collections.singleton(response));
} }