Line data Source code
1 : // Copyright (C) 2010 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.git.receive; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : 19 : import com.google.common.collect.Sets; 20 : import com.google.common.flogger.FluentLogger; 21 : import com.google.gerrit.entities.Account; 22 : import com.google.gerrit.entities.PatchSet; 23 : import com.google.gerrit.entities.Project; 24 : import com.google.gerrit.entities.RefNames; 25 : import com.google.gerrit.exceptions.StorageException; 26 : import com.google.gerrit.index.query.Predicate; 27 : import com.google.gerrit.server.git.HookUtil; 28 : import com.google.gerrit.server.index.change.ChangeField; 29 : import com.google.gerrit.server.query.change.ChangeData; 30 : import com.google.gerrit.server.query.change.ChangePredicates; 31 : import com.google.gerrit.server.query.change.ChangeStatusPredicate; 32 : import com.google.gerrit.server.query.change.InternalChangeQuery; 33 : import com.google.gerrit.server.util.MagicBranch; 34 : import com.google.inject.Provider; 35 : import java.io.IOException; 36 : import java.util.Collections; 37 : import java.util.Map; 38 : import java.util.Set; 39 : import org.eclipse.jgit.lib.ObjectId; 40 : import org.eclipse.jgit.lib.Ref; 41 : import org.eclipse.jgit.lib.Repository; 42 : import org.eclipse.jgit.transport.AdvertiseRefsHook; 43 : import org.eclipse.jgit.transport.ReceivePack; 44 : import org.eclipse.jgit.transport.ServiceMayNotContinueException; 45 : import org.eclipse.jgit.transport.UploadPack; 46 : 47 : /** 48 : * Exposes only the non refs/changes/ reference names and provide additional haves. 49 : * 50 : * <p>Negotiation on Git push is suboptimal in that it tends to send more objects to the server than 51 : * it should. This results in increased latencies for {@code git push}. 52 : * 53 : * <p>Ref advertisement for Git pushes still works in a "the server speaks first fashion" as Git 54 : * Protocol V2 only addressed fetches Therefore the server needs to send all available references. 55 : * For large repositories, this can be in the tens of megabytes to send to the client. We therefore 56 : * remove all refs in refs/changes/* to scale down that footprint. Trivially, this would increase 57 : * the unnecessary objects that the client has to send to the server because the common ancestor it 58 : * finds in negotiation might be further back in history. 59 : * 60 : * <p>To work around this, we advertise the last 32 changes in that repository as additional {@code 61 : * .haves}. This is a heuristical approach that aims at scaling down the number of unnecessary 62 : * objects that client sends to the server. Unnecessary here refers to objects that the server 63 : * already has. 64 : * 65 : * <p>TODO(hiesel): Instrument this heuristic and proof its value. 66 : */ 67 : public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook { 68 97 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 69 : 70 : private final Provider<InternalChangeQuery> queryProvider; 71 : private final Project.NameKey projectName; 72 : private final Account.Id user; 73 : 74 : public ReceiveCommitsAdvertiseRefsHook( 75 97 : Provider<InternalChangeQuery> queryProvider, Project.NameKey projectName, Account.Id user) { 76 97 : this.queryProvider = queryProvider; 77 97 : this.projectName = projectName; 78 97 : this.user = user; 79 97 : } 80 : 81 : @Override 82 : public void advertiseRefs(UploadPack us) { 83 0 : throw new UnsupportedOperationException( 84 : "ReceiveCommitsAdvertiseRefsHook cannot be used for UploadPack"); 85 : } 86 : 87 : @Override 88 : public void advertiseRefs(ReceivePack rp) throws ServiceMayNotContinueException { 89 97 : Map<String, Ref> advertisedRefs = HookUtil.ensureAllRefsAdvertised(rp); 90 97 : advertisedRefs.keySet().stream() 91 97 : .filter(ReceiveCommitsAdvertiseRefsHook::skip) 92 97 : .collect(toImmutableList()) 93 97 : .forEach(r -> advertisedRefs.remove(r)); 94 : try { 95 97 : rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository())); 96 0 : } catch (IOException e) { 97 0 : throw new ServiceMayNotContinueException(e); 98 97 : } 99 97 : } 100 : 101 : private Set<ObjectId> advertiseOpenChanges(Repository repo) 102 : throws ServiceMayNotContinueException { 103 : // Advertise the user's most recent open changes. It's likely that the user has one of these in 104 : // their local repo and they can serve as starting points to figure out the common ancestor of 105 : // what the client and server have in common. 106 97 : int limit = 32; 107 : try { 108 97 : Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit); 109 : for (ChangeData cd : 110 : queryProvider 111 97 : .get() 112 97 : .setRequestedFields( 113 : // Required for ChangeIsVisibleToPrdicate. 114 : ChangeField.CHANGE, 115 : ChangeField.REVIEWER_SPEC, 116 : // Required during advertiseOpenChanges. 117 : ChangeField.PATCH_SET) 118 97 : .enforceVisibility(true) 119 97 : .setLimit(limit) 120 97 : .query( 121 97 : Predicate.and( 122 97 : ChangePredicates.project(projectName), 123 97 : ChangeStatusPredicate.open(), 124 97 : ChangePredicates.owner(user)))) { 125 62 : PatchSet ps = cd.currentPatchSet(); 126 62 : if (ps != null) { 127 : // Ensure we actually observed a patch set ref pointing to this 128 : // object, in case the database is out of sync with the repo and the 129 : // object doesn't actually exist. 130 : try { 131 62 : Ref psRef = repo.getRefDatabase().exactRef(RefNames.patchSetRef(ps.id())); 132 62 : if (psRef != null) { 133 62 : r.add(ps.commitId()); 134 : } 135 0 : } catch (IOException e) { 136 0 : throw new ServiceMayNotContinueException(e); 137 62 : } 138 : } 139 62 : } 140 : 141 97 : return r; 142 0 : } catch (StorageException err) { 143 0 : logger.atSevere().withCause(err).log("Cannot list open changes of %s", projectName); 144 0 : return Collections.emptySet(); 145 : } 146 : } 147 : 148 : private static boolean skip(String name) { 149 97 : return name.startsWith(RefNames.REFS_CHANGES) 150 97 : || name.startsWith(RefNames.REFS_CACHE_AUTOMERGE) 151 97 : || MagicBranch.isMagicBranch(name); 152 : } 153 : }