Merge "Fix ReceiveCommitsAdvertiseRefsHook, add docs and test coverage"

This commit is contained in:
Patrick Hiesel
2019-06-18 15:20:49 +00:00
committed by Gerrit Code Review
8 changed files with 245 additions and 57 deletions

View File

@@ -123,6 +123,11 @@ public class RefNames {
return shard(id.get(), r).append(META_SUFFIX).toString(); return shard(id.get(), r).append(META_SUFFIX).toString();
} }
public static String patchSetRef(PatchSet.Id id) {
StringBuilder r = newStringBuilder().append(REFS_CHANGES);
return shard(id.changeId().get(), r).append('/').append(id.get()).toString();
}
public static String robotCommentsRef(Change.Id id) { public static String robotCommentsRef(Change.Id id) {
StringBuilder r = newStringBuilder().append(REFS_CHANGES); StringBuilder r = newStringBuilder().append(REFS_CHANGES);
return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString(); return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();

View File

@@ -35,12 +35,10 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ReceiveCommitsExecutor; import com.google.gerrit.server.config.ReceiveCommitsExecutor;
import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.MultiProgressMonitor; import com.google.gerrit.server.git.MultiProgressMonitor;
import com.google.gerrit.server.git.ProjectRunnable; import com.google.gerrit.server.git.ProjectRunnable;
import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ContributorAgreementsChecker; import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -58,7 +56,6 @@ import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -66,8 +63,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
import org.eclipse.jgit.transport.PreReceiveHook; import org.eclipse.jgit.transport.PreReceiveHook;
import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.eclipse.jgit.transport.ReceiveCommand.Result;
@@ -297,15 +292,10 @@ public class AsyncReceiveCommits implements PreReceiveHook {
receiveConfig.checkReferencedObjectsAreReachable); receiveConfig.checkReferencedObjectsAreReachable);
} }
List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
allRefsWatcher = new AllRefsWatcher(); allRefsWatcher = new AllRefsWatcher();
advHooks.add(allRefsWatcher); receivePack.setAdvertiseRefsHook(
advHooks.add( ReceiveCommitsAdvertiseRefsHookChain.create(
new DefaultAdvertiseRefsHook(perm, RefFilterOptions.builder().setFilterMeta(true).build())); allRefsWatcher, perm, queryProvider, projectName));
advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
advHooks.add(new HackPushNegotiateHook());
receivePack.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
resultChangeIds = new ResultChangeIds(); resultChangeIds = new ResultChangeIds();
receiveCommits = receiveCommits =
factory.create( factory.create(

View File

@@ -14,9 +14,8 @@
package com.google.gerrit.server.git.receive; package com.google.gerrit.server.git.receive;
import com.google.auto.value.AutoValue; import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.gerrit.exceptions.StorageException; import com.google.gerrit.exceptions.StorageException;
@@ -29,28 +28,44 @@ import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.AdvertiseRefsHook; import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.BaseReceivePack; import org.eclipse.jgit.transport.BaseReceivePack;
import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.ServiceMayNotContinueException;
import org.eclipse.jgit.transport.UploadPack; import org.eclipse.jgit.transport.UploadPack;
/** Exposes only the non refs/changes/ reference names. */ /**
* Exposes only the non refs/changes/ reference names and provide additional haves.
*
* <p>Negotiation on Git push is suboptimal in that it tends to send more objects to the server than
* it should. This results in increased latencies for {@code git push}.
*
* <p>Ref advertisement for Git pushes still works in a "the server speaks first fashion" as Git
* Protocol V2 only addressed fetches Therefore the server needs to send all available references.
* For large repositories, this can be in the tens of megabytes to send to the client. We therefore
* remove all refs in refs/changes/* to scale down that footprint. Trivially, this would increase
* the unnecessary objects that the client has to send to the server because the common ancestor it
* finds in negotiation might be further back in history.
*
* <p>To work around this, we advertise the last 32 changes in that repository as additional {@code
* .haves}. This is a heuristical approach that aims at scaling down the number of unnecessary
* objects that client sends to the server. Unnecessary here refers to objects that the server
* already has.
*
* <p>For some code paths in {@link com.google.gerrit.server.git.DefaultAdvertiseRefsHook}, we
* already removed refs/changes, so the logic to skip these in this class become a no-op.
*
* <p>TODO(hiesel): Instrument this heuristic and proof its value.
*/
public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook { public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting
@AutoValue
public abstract static class Result {
public abstract Map<String, Ref> allRefs();
public abstract Set<ObjectId> additionalHaves();
}
private final Provider<InternalChangeQuery> queryProvider; private final Provider<InternalChangeQuery> queryProvider;
private final Project.NameKey projectName; private final Project.NameKey projectName;
@@ -68,28 +83,16 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
@Override @Override
public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException { public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
Result r = advertiseRefs(HookUtil.ensureAllRefsAdvertised(rp)); Map<String, Ref> advertisedRefs = HookUtil.ensureAllRefsAdvertised(rp);
rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves()); advertisedRefs.keySet().stream()
.filter(ReceiveCommitsAdvertiseRefsHook::skip)
.collect(toImmutableList())
.forEach(r -> advertisedRefs.remove(r));
rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository()));
} }
@VisibleForTesting private Set<ObjectId> advertiseOpenChanges(Repository repo)
public Result advertiseRefs(Map<String, Ref> oldRefs) { throws ServiceMayNotContinueException {
Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
Set<ObjectId> allPatchSets = Sets.newHashSetWithExpectedSize(oldRefs.size());
for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
String name = e.getKey();
if (!skip(name)) {
r.put(name, e.getValue());
}
if (name.startsWith(RefNames.REFS_CHANGES)) {
allPatchSets.add(e.getValue().getObjectId());
}
}
return new AutoValue_ReceiveCommitsAdvertiseRefsHook_Result(
r, advertiseOpenChanges(allPatchSets));
}
private Set<ObjectId> advertiseOpenChanges(Set<ObjectId> allPatchSets) {
// Advertise some recent open changes, in case a commit is based on one. // Advertise some recent open changes, in case a commit is based on one.
int limit = 32; int limit = 32;
try { try {
@@ -111,11 +114,17 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
// Ensure we actually observed a patch set ref pointing to this // Ensure we actually observed a patch set ref pointing to this
// object, in case the database is out of sync with the repo and the // object, in case the database is out of sync with the repo and the
// object doesn't actually exist. // object doesn't actually exist.
if (allPatchSets.contains(ps.commitId())) { try {
r.add(ps.commitId()); Ref psRef = repo.getRefDatabase().exactRef(RefNames.patchSetRef(ps.id()));
if (psRef != null) {
r.add(ps.commitId());
}
} catch (IOException e) {
throw new ServiceMayNotContinueException(e);
} }
} }
} }
return r; return r;
} catch (StorageException err) { } catch (StorageException err) {
logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName); logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);

View File

@@ -0,0 +1,76 @@
// 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.git.receive;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
/**
* Helper to ensure that the chain for advertising refs is the same in tests and production code.
*/
public class ReceiveCommitsAdvertiseRefsHookChain {
/**
* Returns a single {@link AdvertiseRefsHook} that encompasses a chain of {@link
* AdvertiseRefsHook} to be used for advertising when processing a Git push.
*/
public static AdvertiseRefsHook create(
AllRefsWatcher allRefsWatcher,
PermissionBackend.ForProject perm,
Provider<InternalChangeQuery> queryProvider,
Project.NameKey projectName) {
return create(allRefsWatcher, perm, queryProvider, projectName, false);
}
/**
* Returns a single {@link AdvertiseRefsHook} that encompasses a chain of {@link
* AdvertiseRefsHook} to be used for advertising when processing a Git push. Omits {@link
* HackPushNegotiateHook} as that does not advertise refs on it's own but adds {@code .have} based
* on history which is not relevant for the tests we have.
*/
@VisibleForTesting
public static AdvertiseRefsHook createForTest(
PermissionBackend.ForProject perm,
Provider<InternalChangeQuery> queryProvider,
Project.NameKey projectName) {
return create(new AllRefsWatcher(), perm, queryProvider, projectName, true);
}
private static AdvertiseRefsHook create(
AllRefsWatcher allRefsWatcher,
PermissionBackend.ForProject perm,
Provider<InternalChangeQuery> queryProvider,
Project.NameKey projectName,
boolean skipHackPushNegotiateHook) {
List<AdvertiseRefsHook> advHooks = new ArrayList<>();
advHooks.add(allRefsWatcher);
advHooks.add(
new DefaultAdvertiseRefsHook(perm, RefFilterOptions.builder().setFilterMeta(true).build()));
advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
if (!skipHackPushNegotiateHook) {
advHooks.add(new HackPushNegotiateHook());
}
return AdvertiseRefsHookChain.newChain(advHooks);
}
}

View File

@@ -0,0 +1,12 @@
java_library(
name = "testing",
testonly = 1,
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//lib:guava",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/jgit/org.eclipse.jgit:jgit",
],
)

View File

@@ -0,0 +1,87 @@
// 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.git.receive.testing;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.StreamSupport;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RefAdvertiser;
/** Helper to collect advertised refs and additonal haves and verify them in tests. */
public class TestRefAdvertiser extends RefAdvertiser {
@VisibleForTesting
@AutoValue
public abstract static class Result {
public abstract Map<String, Ref> allRefs();
public abstract Set<ObjectId> additionalHaves();
public static Result create(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves) {
return new AutoValue_TestRefAdvertiser_Result(allRefs, additionalHaves);
}
}
private final Map<String, Ref> advertisedRefs;
private final Set<ObjectId> additionalHaves;
private final Repository repo;
public TestRefAdvertiser(Repository repo) {
advertisedRefs = new HashMap<>();
additionalHaves = new HashSet<>();
this.repo = repo;
}
@Override
protected void writeOne(CharSequence line) throws IOException {
List<String> lineParts =
StreamSupport.stream(Splitter.on(' ').split(line).spliterator(), false)
.map(String::trim)
.collect(toImmutableList());
if (".have".equals(lineParts.get(1))) {
additionalHaves.add(ObjectId.fromString(lineParts.get(0)));
} else {
ObjectId id = ObjectId.fromString(lineParts.get(0));
Ref ref =
repo.getRefDatabase().getRefs().stream()
.filter(r -> r.getObjectId().equals(id))
.findAny()
.orElseThrow(
() ->
new RuntimeException(
line.toString() + " does not conform to expected pattern"));
advertisedRefs.put(lineParts.get(1), ref);
}
}
@Override
protected void end() {}
public Result result() {
return Result.create(advertisedRefs, additionalHaves);
}
}

View File

@@ -8,6 +8,7 @@ load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
":push_for_review", ":push_for_review",
":submodule_util", ":submodule_util",
"//java/com/google/gerrit/git", "//java/com/google/gerrit/git",
"//java/com/google/gerrit/server/git/receive/testing",
"//lib/commons:lang", "//lib/commons:lang",
], ],
) for f in glob(["*IT.java"])] ) for f in glob(["*IT.java"])]

View File

@@ -50,7 +50,8 @@ import com.google.gerrit.reviewdb.client.PatchSet;
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.config.AllUsersName; import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook; import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHookChain;
import com.google.gerrit.server.git.receive.testing.TestRefAdvertiser;
import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.Sequences; import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackend;
@@ -75,6 +76,8 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.ReceivePack;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -544,11 +547,10 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
@Test @Test
public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception { public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception {
ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs(); TestRefAdvertiser.Result r = getReceivePackRefs(admin);
assertThat(r.allRefs().keySet()) assertThat(r.allRefs().keySet())
.containsExactly( .containsExactly(
// meta refs are excluded // meta refs are excluded
"HEAD",
"refs/heads/branch", "refs/heads/branch",
"refs/heads/master", "refs/heads/master",
"refs/meta/config", "refs/meta/config",
@@ -568,7 +570,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
.update(); .update();
requestScopeOperations.setApiUser(user.id()); requestScopeOperations.setApiUser(user.id());
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 1)); assertThat(getReceivePackRefs(user).additionalHaves()).containsExactly(obj(cd3, 1));
} }
@Test @Test
@@ -577,7 +579,8 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
PushOneCommit.Result r = amendChange(cd3.change().getKey().get()); PushOneCommit.Result r = amendChange(cd3.change().getKey().get());
r.assertOkStatus(); r.assertOkStatus();
cd3 = r.getChange(); cd3 = r.getChange();
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd3, 2), obj(cd4, 1)); assertThat(getReceivePackRefs(admin).additionalHaves())
.containsExactly(obj(cd3, 2), obj(cd4, 1));
} }
@Test @Test
@@ -615,7 +618,7 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
indexer.index(c.getProject(), c.getId()); indexer.index(c.getProject(), c.getId());
} }
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(cd4, 1)); assertThat(getReceivePackRefs(admin).additionalHaves()).containsExactly(obj(cd4, 1));
} }
@Test @Test
@@ -961,11 +964,16 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
} }
} }
private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs() throws Exception { private TestRefAdvertiser.Result getReceivePackRefs(TestAccount u) throws Exception {
ReceiveCommitsAdvertiseRefsHook hook =
new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
try (Repository repo = repoManager.openRepository(project)) { try (Repository repo = repoManager.openRepository(project)) {
return hook.advertiseRefs(getAllRefs(repo)); AdvertiseRefsHook adv =
ReceiveCommitsAdvertiseRefsHookChain.createForTest(
newFilter(project, u), queryProvider, project);
ReceivePack rp = new ReceivePack(repo);
rp.setAdvertiseRefsHook(adv);
TestRefAdvertiser advertiser = new TestRefAdvertiser(repo);
rp.sendAdvertisedRefs(advertiser);
return advertiser.result();
} }
} }