Merge "Fix ReceiveCommitsAdvertiseRefsHook, add docs and test coverage"
This commit is contained in:
@@ -123,6 +123,11 @@ public class RefNames {
|
||||
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) {
|
||||
StringBuilder r = newStringBuilder().append(REFS_CHANGES);
|
||||
return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();
|
||||
|
||||
@@ -35,12 +35,10 @@ import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.config.ConfigUtil;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
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.ProjectRunnable;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
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.ProjectPermission;
|
||||
import com.google.gerrit.server.project.ContributorAgreementsChecker;
|
||||
@@ -58,7 +56,6 @@ import com.google.inject.assistedinject.FactoryModuleBuilder;
|
||||
import com.google.inject.name.Named;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -66,8 +63,6 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
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.ReceiveCommand;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand.Result;
|
||||
@@ -297,15 +292,10 @@ public class AsyncReceiveCommits implements PreReceiveHook {
|
||||
receiveConfig.checkReferencedObjectsAreReachable);
|
||||
}
|
||||
|
||||
List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
|
||||
allRefsWatcher = new AllRefsWatcher();
|
||||
advHooks.add(allRefsWatcher);
|
||||
advHooks.add(
|
||||
new DefaultAdvertiseRefsHook(perm, RefFilterOptions.builder().setFilterMeta(true).build()));
|
||||
advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
|
||||
advHooks.add(new HackPushNegotiateHook());
|
||||
receivePack.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
|
||||
|
||||
receivePack.setAdvertiseRefsHook(
|
||||
ReceiveCommitsAdvertiseRefsHookChain.create(
|
||||
allRefsWatcher, perm, queryProvider, projectName));
|
||||
resultChangeIds = new ResultChangeIds();
|
||||
receiveCommits =
|
||||
factory.create(
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
|
||||
package com.google.gerrit.server.git.receive;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Maps;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
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.util.MagicBranch;
|
||||
import com.google.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.AdvertiseRefsHook;
|
||||
import org.eclipse.jgit.transport.BaseReceivePack;
|
||||
import org.eclipse.jgit.transport.ServiceMayNotContinueException;
|
||||
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 {
|
||||
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 Project.NameKey projectName;
|
||||
|
||||
@@ -68,28 +83,16 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
|
||||
|
||||
@Override
|
||||
public void advertiseRefs(BaseReceivePack rp) throws ServiceMayNotContinueException {
|
||||
Result r = advertiseRefs(HookUtil.ensureAllRefsAdvertised(rp));
|
||||
rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves());
|
||||
Map<String, Ref> advertisedRefs = HookUtil.ensureAllRefsAdvertised(rp);
|
||||
advertisedRefs.keySet().stream()
|
||||
.filter(ReceiveCommitsAdvertiseRefsHook::skip)
|
||||
.collect(toImmutableList())
|
||||
.forEach(r -> advertisedRefs.remove(r));
|
||||
rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Result advertiseRefs(Map<String, Ref> oldRefs) {
|
||||
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) {
|
||||
private Set<ObjectId> advertiseOpenChanges(Repository repo)
|
||||
throws ServiceMayNotContinueException {
|
||||
// Advertise some recent open changes, in case a commit is based on one.
|
||||
int limit = 32;
|
||||
try {
|
||||
@@ -111,11 +114,17 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
|
||||
// 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 doesn't actually exist.
|
||||
if (allPatchSets.contains(ps.commitId())) {
|
||||
r.add(ps.commitId());
|
||||
try {
|
||||
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;
|
||||
} catch (StorageException err) {
|
||||
logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
12
java/com/google/gerrit/server/git/receive/testing/BUILD
Normal file
12
java/com/google/gerrit/server/git/receive/testing/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user