blob: c54fe262c2413bcd36e6d12c38a24ff2de3b4d6f [file] [log] [blame]
// Copyright (C) 2015 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;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
/**
* Context for a set of updates that should be applied for a site.
* <p>
* An update operation can be divided into three phases:
* <ol>
* <li>Git reference updates</li>
* <li>Database updates</li>
* <li>Post-update steps<li>
* </ol>
* A single conceptual operation, such as a REST API call or a merge operation,
* may make multiple changes at each step, which all need to be serialized
* relative to each other. Moreover, for consistency, <em>all</em> git ref
* updates must be performed before <em>any</em> database updates, since
* database updates might refer to newly-created patch set refs. And all
* post-update steps, such as hooks, should run only after all storage
* mutations have completed.
* <p>
* Depending on the backend used, each step might support batching, for example
* in a {@code BatchRefUpdate} or one or more database transactions. All
* operations in one phase must complete successfully before proceeding to the
* next phase.
*/
public class BatchUpdate implements AutoCloseable {
public interface Factory {
public BatchUpdate create(ReviewDb db, Project.NameKey project,
CurrentUser user, Timestamp when);
}
public class Context {
public Project.NameKey getProject() {
return project;
}
public Timestamp getWhen() {
return when;
}
public ReviewDb getDb() {
return db;
}
public CurrentUser getUser() {
return user;
}
}
public class RepoContext extends Context {
public Repository getRepository() throws IOException {
initRepository();
return repo;
}
public RevWalk getRevWalk() throws IOException {
initRepository();
return revWalk;
}
public ObjectInserter getInserter() throws IOException {
initRepository();
return inserter;
}
public BatchRefUpdate getBatchRefUpdate() throws IOException {
initRepository();
if (batchRefUpdate == null) {
batchRefUpdate = repo.getRefDatabase().newBatchUpdate();
}
return batchRefUpdate;
}
public void addRefUpdate(ReceiveCommand cmd) throws IOException {
getBatchRefUpdate().addCommand(cmd);
}
public TimeZone getTimeZone() {
return tz;
}
}
public class ChangeContext extends Context {
private final ChangeControl ctl;
private final ChangeUpdate update;
private ChangeContext(ChangeControl ctl) {
this.ctl = ctl;
this.update = changeUpdateFactory.create(ctl, when);
}
public ChangeUpdate getChangeUpdate() {
return update;
}
public ChangeNotes getChangeNotes() {
return update.getChangeNotes();
}
public ChangeControl getChangeControl() {
return ctl;
}
public Change getChange() {
return update.getChange();
}
}
public static class Op {
@SuppressWarnings("unused")
public void updateRepo(RepoContext ctx) throws Exception {
}
@SuppressWarnings("unused")
public void updateChange(ChangeContext ctx) throws Exception {
}
// TODO(dborowitz): Support async operations?
@SuppressWarnings("unused")
public void postUpdate(Context ctx) throws Exception {
}
}
public abstract static class InsertChangeOp extends Op {
public abstract Change getChange();
}
private final ReviewDb db;
private final GitRepositoryManager repoManager;
private final ChangeIndexer indexer;
private final ChangeControl.GenericFactory changeControlFactory;
private final ChangeUpdate.Factory changeUpdateFactory;
private final GitReferenceUpdated gitRefUpdated;
private final Project.NameKey project;
private final CurrentUser user;
private final Timestamp when;
private final TimeZone tz;
private final ListMultimap<Change.Id, Op> ops = ArrayListMultimap.create();
private final Map<Change.Id, Change> newChanges = new HashMap<>();
private final List<CheckedFuture<?, IOException>> indexFutures =
new ArrayList<>();
private Repository repo;
private ObjectInserter inserter;
private RevWalk revWalk;
private BatchRefUpdate batchRefUpdate;
private boolean closeRepo;
@AssistedInject
BatchUpdate(GitRepositoryManager repoManager,
ChangeIndexer indexer,
ChangeControl.GenericFactory changeControlFactory,
ChangeUpdate.Factory changeUpdateFactory,
GitReferenceUpdated gitRefUpdated,
@GerritPersonIdent PersonIdent serverIdent,
@Assisted ReviewDb db,
@Assisted Project.NameKey project,
@Assisted CurrentUser user,
@Assisted Timestamp when) {
this.db = db;
this.repoManager = repoManager;
this.indexer = indexer;
this.changeControlFactory = changeControlFactory;
this.changeUpdateFactory = changeUpdateFactory;
this.gitRefUpdated = gitRefUpdated;
this.project = project;
this.user = user;
this.when = when;
tz = serverIdent.getTimeZone();
}
@Override
public void close() {
if (closeRepo) {
revWalk.close();
inserter.close();
repo.close();
}
}
public BatchUpdate setRepository(Repository repo, RevWalk revWalk,
ObjectInserter inserter) {
checkState(this.repo == null, "repo already set");
closeRepo = false;
this.repo = checkNotNull(repo, "repo");
this.revWalk = checkNotNull(revWalk, "revWalk");
this.inserter = checkNotNull(inserter, "inserter");
return this;
}
private void initRepository() throws IOException {
if (repo == null) {
this.repo = repoManager.openRepository(project);
closeRepo = true;
inserter = repo.newObjectInserter();
revWalk = new RevWalk(inserter.newReader());
}
}
public CurrentUser getUser() {
return user;
}
public Repository getRepository() throws IOException {
initRepository();
return repo;
}
public RevWalk getRevWalk() throws IOException {
initRepository();
return revWalk;
}
public ObjectInserter getObjectInserter() throws IOException {
initRepository();
return inserter;
}
public BatchUpdate addOp(Change.Id id, Op op) {
checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
ops.put(id, op);
return this;
}
public BatchUpdate insertChange(InsertChangeOp op) {
Change c = op.getChange();
checkArgument(!newChanges.containsKey(c.getId()),
"only one op allowed to create change %s", c.getId());
newChanges.put(c.getId(), c);
ops.get(c.getId()).add(0, op);
return this;
}
public void execute() throws UpdateException, RestApiException {
try {
executeRefUpdates();
executeChangeOps();
reindexChanges();
if (batchRefUpdate != null) {
// Fire ref update events only after all mutations are finished, since
// callers may assume a patch set ref being created means the change was
// created, or a branch advancing meaning some changes were closed.
gitRefUpdated.fire(project, batchRefUpdate);
}
executePostOps();
} catch (UpdateException | RestApiException e) {
// Propagate REST API exceptions thrown by operations; they commonly throw
// exceptions like ResourceConflictException to indicate an atomic update
// failure.
throw e;
} catch (Exception e) {
Throwables.propagateIfPossible(e);
throw new UpdateException(e);
}
}
private void executeRefUpdates()
throws IOException, UpdateException, RestApiException {
try {
RepoContext ctx = new RepoContext();
for (Op op : ops.values()) {
op.updateRepo(ctx);
}
} catch (Exception e) {
Throwables.propagateIfPossible(e, RestApiException.class);
throw new UpdateException(e);
}
if (repo == null || batchRefUpdate == null
|| batchRefUpdate.getCommands().isEmpty()) {
return;
}
inserter.flush();
batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
boolean ok = true;
for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
if (cmd.getResult() != ReceiveCommand.Result.OK) {
ok = false;
break;
}
}
if (!ok) {
throw new UpdateException("BatchRefUpdate failed: " + batchRefUpdate);
}
}
private void executeChangeOps() throws UpdateException, RestApiException {
try {
for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
Change.Id id = e.getKey();
db.changes().beginTransaction(id);
ChangeContext ctx;
try {
ctx = newChangeContext(id);
for (Op op : e.getValue()) {
op.updateChange(ctx);
}
db.commit();
} finally {
db.rollback();
}
ctx.getChangeUpdate().commit();
indexFutures.add(indexer.indexAsync(id));
}
} catch (Exception e) {
Throwables.propagateIfPossible(e, RestApiException.class);
throw new UpdateException(e);
}
}
private ChangeContext newChangeContext(Change.Id id) throws Exception {
Change c = newChanges.get(id);
if (c == null) {
c = db.changes().get(id);
}
// Pass in preloaded change to controlFor, to avoid:
// - reading from a db that does not belong to this update
// - attempting to read a change that doesn't exist yet
return new ChangeContext(
changeControlFactory.controlFor(c, user));
}
private void reindexChanges() throws IOException {
ChangeIndexer.allAsList(indexFutures).checkedGet();
}
private void executePostOps() throws Exception {
Context ctx = new Context();
for (Op op : ops.values()) {
op.postUpdate(ctx);
}
}
}