BatchUpdate: Add a combined operation spanning all phases

A common pattern thus far has been to communicate between ChangeOps
and subsequent Callables using AtomicReferences in the caller. This
works, but using so many AtomicReferences smells kind of wrong,
particularly since the caller-provided portions of BatchUpdate are
single-threaded.

Instead, create a single class Op, which may have steps for each of
the several phases, including a new repo-updating phase. Each of these
steps is passed in a Context object, which includes a phase-specific
view of the BatchUpdate. This encapsulation prevents operations from
doing unsupported things like adding new operations in the middle of
the execute process.

Ops can now communicate between different phases or with the caller
using instance variables.

Change-Id: Id97dbb772d2e3051d6a74bb8e819d691843a45e1
This commit is contained in:
Dave Borowitz
2015-02-20 09:26:46 -08:00
parent 4ddb18d0ba
commit 555ae63f86
6 changed files with 417 additions and 367 deletions

View File

@@ -19,7 +19,6 @@ import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -32,9 +31,9 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.PutTopic.Input;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeOp;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -42,10 +41,7 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
@Singleton
public class PutTopic implements RestModifyView<ChangeResource, Input>,
@@ -75,73 +71,73 @@ public class PutTopic implements RestModifyView<ChangeResource, Input>,
public Response<String> apply(ChangeResource req, Input input)
throws AuthException, UpdateException, RestApiException, OrmException,
IOException {
if (input == null) {
input = new Input();
}
final String inputTopic = input.topic;
ChangeControl control = req.getControl();
if (!control.canEditTopicName()) {
ChangeControl ctl = req.getControl();
if (!ctl.canEditTopicName()) {
throw new AuthException("changing topic not permitted");
}
final Change.Id id = req.getChange().getId();
final IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
final AtomicReference<Change> change = new AtomicReference<>();
final AtomicReference<String> oldTopicName = new AtomicReference<>();
final AtomicReference<String> newTopicName = new AtomicReference<>();
final Timestamp now = TimeUtil.nowTs();
Op op = new Op(ctl, input != null ? input : new Input());
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
req.getChange().getProject(), now)) {
u.addChangeOp(new ChangeOp(req.getControl()) {
@Override
public void call(ReviewDb db, ChangeUpdate update) throws OrmException,
ResourceConflictException {
Change c = db.changes().get(id);
String n = Strings.nullToEmpty(inputTopic);
String o = Strings.nullToEmpty(c.getTopic());
if (o.equals(n)) {
return;
}
String summary;
if (o.isEmpty()) {
summary = "Topic set to " + n;
} else if (n.isEmpty()) {
summary = "Topic " + o + " removed";
} else {
summary = String.format("Topic changed from %s to %s", o, n);
}
c.setTopic(Strings.emptyToNull(n));
ChangeUtil.updated(c);
db.changes().update(Collections.singleton(c));
ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)),
caller.getAccountId(), now, c.currentPatchSetId());
cmsg.setMessage(summary);
cmUtil.addChangeMessage(db, update, cmsg);
change.set(c);
oldTopicName.set(o);
newTopicName.set(n);
}
});
u.addPostOp(new Callable<Void>() {
@Override
public Void call() throws OrmException {
Change c = change.get();
if (c != null) {
hooks.doTopicChangedHook(change.get(), caller.getAccount(),
oldTopicName.get(), dbProvider.get());
}
return null;
}
});
req.getChange().getProject(), TimeUtil.nowTs())) {
u.addOp(ctl, op);
u.execute();
}
String n = newTopicName.get();
return Strings.isNullOrEmpty(n) ? Response.<String> none() : Response.ok(n);
return Strings.isNullOrEmpty(op.newTopicName)
? Response.<String> none()
: Response.ok(op.newTopicName);
}
private class Op extends BatchUpdate.Op {
private final Input input;
private final IdentifiedUser caller;
private Change change;
private String oldTopicName;
private String newTopicName;
public Op(ChangeControl ctl, Input input) {
this.input = input;
this.caller = (IdentifiedUser) ctl.getCurrentUser();
}
@Override
public void updateChange(ChangeContext ctx) throws OrmException {
change = ctx.readChange();
String newTopicName = Strings.nullToEmpty(input.topic);
String oldTopicName = Strings.nullToEmpty(change.getTopic());
if (oldTopicName.equals(newTopicName)) {
return;
}
String summary;
if (oldTopicName.isEmpty()) {
summary = "Topic set to " + newTopicName;
} else if (newTopicName.isEmpty()) {
summary = "Topic " + oldTopicName + " removed";
} else {
summary = String.format("Topic changed from %s to %s",
oldTopicName, newTopicName);
}
change.setTopic(Strings.emptyToNull(newTopicName));
ChangeUtil.updated(change);
ctx.getDb().changes().update(Collections.singleton(change));
ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key(
change.getId(),
ChangeUtil.messageUUID(ctx.getDb())),
caller.getAccountId(), ctx.getWhen(),
change.currentPatchSetId());
cmsg.setMessage(summary);
cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(), cmsg);
}
@Override
public void postUpdate(Context ctx) throws OrmException {
if (change != null) {
hooks.doTopicChangedHook(change, caller.getAccount(),
oldTopicName, ctx.getDb());
}
}
}
@Override