blob: 76dfdc8a1377fe1f6c660f620d883d24cc3bdf36 [file] [log] [blame]
// Copyright (C) 2014 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.notedb;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
public class ChangeRebuilder {
private static final long TS_WINDOW_MS =
TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
private final Provider<ReviewDb> dbProvider;
private final ChangeControl.GenericFactory controlFactory;
private final IdentifiedUser.GenericFactory userFactory;
private final PatchListCache patchListCache;
private final ChangeUpdate.Factory updateFactory;
private final ChangeDraftUpdate.Factory draftUpdateFactory;
@Inject
ChangeRebuilder(Provider<ReviewDb> dbProvider,
ChangeControl.GenericFactory controlFactory,
IdentifiedUser.GenericFactory userFactory,
PatchListCache patchListCache,
ChangeUpdate.Factory updateFactory,
ChangeDraftUpdate.Factory draftUpdateFactory) {
this.dbProvider = dbProvider;
this.controlFactory = controlFactory;
this.userFactory = userFactory;
this.patchListCache = patchListCache;
this.updateFactory = updateFactory;
this.draftUpdateFactory = draftUpdateFactory;
}
public ListenableFuture<?> rebuildAsync(final Change change,
ListeningExecutorService executor, final BatchRefUpdate bru,
final BatchRefUpdate bruForDrafts, final Repository changeRepo,
final Repository allUsersRepo) {
return executor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
rebuild(change, bru, bruForDrafts, changeRepo, allUsersRepo);
return null;
}
});
}
public void rebuild(Change change, BatchRefUpdate bru,
BatchRefUpdate bruForDrafts, Repository changeRepo,
Repository allUsersRepo) throws NoSuchChangeException, IOException,
OrmException {
deleteRef(change, changeRepo);
ReviewDb db = dbProvider.get();
Change.Id changeId = change.getId();
// We will rebuild all events, except for draft comments, in buckets based
// on author and timestamp. However, all draft comments for a given change
// and author will be written as one commit in the notedb.
List<Event> events = Lists.newArrayList();
Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
ArrayListMultimap.create();
for (PatchSet ps : db.patchSets().byChange(changeId)) {
events.add(new PatchSetEvent(ps));
for (PatchLineComment c : db.patchComments().byPatchSet(ps.getId())) {
PatchLineCommentEvent e =
new PatchLineCommentEvent(c, change, ps, patchListCache);
if (c.getStatus() == Status.PUBLISHED) {
events.add(e);
} else {
draftCommentEvents.put(c.getAuthor(), e);
}
}
}
for (PatchSetApproval psa : db.patchSetApprovals().byChange(changeId)) {
events.add(new ApprovalEvent(psa));
}
Collections.sort(events);
BatchMetaDataUpdate batch = null;
ChangeUpdate update = null;
for (Event e : events) {
if (!sameUpdate(e, update)) {
if (update != null) {
writeToBatch(batch, update, changeRepo);
}
IdentifiedUser user = userFactory.create(dbProvider, e.who);
update = updateFactory.create(
controlFactory.controlFor(change, user), e.when);
update.setPatchSetId(e.psId);
if (batch == null) {
batch = update.openUpdateInBatch(bru);
}
}
e.apply(update);
}
if (batch != null) {
if (update != null) {
writeToBatch(batch, update, changeRepo);
}
// Since the BatchMetaDataUpdates generated by all ChangeRebuilders on a
// given project are backed by the same BatchRefUpdate, we need to
// synchronize on the BatchRefUpdate. Therefore, since commit on a
// BatchMetaDataUpdate is the only method that modifies a BatchRefUpdate,
// we can just synchronize this call.
synchronized (bru) {
batch.commit();
}
}
for (Account.Id author : draftCommentEvents.keys()) {
IdentifiedUser user = userFactory.create(dbProvider, author);
ChangeDraftUpdate draftUpdate = null;
BatchMetaDataUpdate batchForDrafts = null;
for (PatchLineCommentEvent e : draftCommentEvents.get(author)) {
if (draftUpdate == null) {
draftUpdate = draftUpdateFactory.create(
controlFactory.controlFor(change, user), e.when);
draftUpdate.setPatchSetId(e.psId);
batchForDrafts = draftUpdate.openUpdateInBatch(bruForDrafts);
}
e.applyDraft(draftUpdate);
}
writeToBatch(batchForDrafts, draftUpdate, allUsersRepo);
synchronized(bruForDrafts) {
batchForDrafts.commit();
}
}
}
private void deleteRef(Change change, Repository changeRepo)
throws IOException {
String refName = ChangeNoteUtil.changeRefName(change.getId());
RefUpdate ru = changeRepo.updateRef(refName, true);
ru.setForceUpdate(true);
RefUpdate.Result result = ru.delete();
switch (result) {
case FORCED:
case NEW:
case NO_CHANGE:
break;
default:
throw new IOException(
String.format("Failed to delete ref %s: %s", refName, result));
}
}
private void writeToBatch(BatchMetaDataUpdate batch,
AbstractChangeUpdate update, Repository repo) throws IOException,
OrmException {
try (ObjectInserter inserter = repo.newObjectInserter()) {
update.setInserter(inserter);
update.writeCommit(batch);
}
}
private static long round(Date when) {
return when.getTime() / TS_WINDOW_MS;
}
private static boolean sameUpdate(Event event, ChangeUpdate update) {
return update != null
&& round(event.when) == round(update.getWhen())
&& event.who.equals(update.getUser().getAccountId())
&& event.psId.equals(update.getPatchSetId());
}
private abstract static class Event implements Comparable<Event> {
final PatchSet.Id psId;
final Account.Id who;
final Timestamp when;
protected Event(PatchSet.Id psId, Account.Id who, Timestamp when) {
this.psId = psId;
this.who = who;
this.when = when;
}
protected void checkUpdate(AbstractChangeUpdate update) {
checkState(Objects.equals(update.getPatchSetId(), psId),
"cannot apply event for %s to update for %s",
update.getPatchSetId(), psId);
checkState(when.getTime() - update.getWhen().getTime() <= TS_WINDOW_MS,
"event at %s outside update window starting at %s",
when, update.getWhen());
checkState(Objects.equals(update.getUser().getAccountId(), who),
"cannot apply event by %s to update by %s",
who, update.getUser().getAccountId());
}
abstract void apply(ChangeUpdate update) throws OrmException;
@Override
public int compareTo(Event other) {
return ComparisonChain.start()
// TODO(dborowitz): Smarter bucketing: pick a bucket start time T and
// include all events up to T + TS_WINDOW_MS but no further.
// Interleaving different authors complicates things.
.compare(round(when), round(other.when))
.compare(who.get(), other.who.get())
.compare(psId.get(), other.psId.get())
.result();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("psId", psId)
.add("who", who)
.add("when", when)
.toString();
}
}
private static class ApprovalEvent extends Event {
private PatchSetApproval psa;
ApprovalEvent(PatchSetApproval psa) {
super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted());
this.psa = psa;
}
@Override
void apply(ChangeUpdate update) {
checkUpdate(update);
update.putApproval(psa.getLabel(), psa.getValue());
}
}
private static class PatchSetEvent extends Event {
private final PatchSet ps;
PatchSetEvent(PatchSet ps) {
super(ps.getId(), ps.getUploader(), ps.getCreatedOn());
this.ps = ps;
}
@Override
void apply(ChangeUpdate update) {
checkUpdate(update);
if (ps.getPatchSetId() == 1) {
update.setSubject("Create change");
} else {
update.setSubject("Create patch set " + ps.getPatchSetId());
}
}
}
private static class PatchLineCommentEvent extends Event {
public final PatchLineComment c;
private final Change change;
private final PatchSet ps;
private final PatchListCache cache;
PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
PatchListCache cache) {
super(getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
this.c = c;
this.change = change;
this.ps = ps;
this.cache = cache;
}
@Override
void apply(ChangeUpdate update) throws OrmException {
checkUpdate(update);
if (c.getRevId() == null) {
setCommentRevId(c, cache, change, ps);
}
update.insertComment(c);
}
void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
if (c.getRevId() == null) {
setCommentRevId(c, cache, change, ps);
}
draftUpdate.insertComment(c);
}
}
}