BatchUpdate: Update/insert/delete change exactly once

The gwtorm backend for gerrit-review has the very unfortunate property
of not supporting multiple updates to the same entity in one
transaction. This has always been the case, but the recent
reorganization of most update code into composable BatchUpdate.Ops has
brought the issue to the fore: different Ops in different locations
may update different parts of an entity, and may all reasonably want
to call changes().update(c) to save their updates.

Prior to the BatchUpdate refactoring, this was not a problem, because
there was less use of transactions. But now that we're doing more
things in transactions (which, remember, is generally a good thing for
performance reasons), this is more likely to crop up.

Wrap the ReviewDb available to ChangeContext with one that does not
support directly modifying the changes table, and replace it with a
handful of *idempotent* methods for indicating that the change should
be updated later. This loses the ability to read back changes that
were previously written in the transaction, but since all Ops should
share a common ChangeContext instance and hence a common mutable
Change instance, they can just read from that.

This change only solves the problem for the Changes table. However,
that should be the most common place where this crops up, since there
are so many fields in Change. Other operations tend to modify
non-overlapping entities (Idd16eaef notwithstanding), for example
one op inserts a new PatchSet and another op adds a ChangeMessage.

Change-Id: Ie7236c2630707df70d452b3f9c014d5591e36aaf
This commit is contained in:
Dave Borowitz
2016-01-15 10:13:53 -05:00
parent feda0ff046
commit fd6ace7595
14 changed files with 402 additions and 25 deletions

View File

@@ -0,0 +1,275 @@
// Copyright (C) 2016 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.reviewdb.server;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.gwtorm.server.StatementExecutor;
import java.util.Map;
public class ReviewDbWrapper implements ReviewDb {
protected final ReviewDb delegate;
protected ReviewDbWrapper(ReviewDb delegate) {
this.delegate = checkNotNull(delegate);
}
@Override
public void commit() throws OrmException {
delegate.commit();
}
@Override
public void rollback() throws OrmException {
delegate.rollback();
}
@Override
public void updateSchema(StatementExecutor e) throws OrmException {
delegate.updateSchema(e);
}
@Override
public void pruneSchema(StatementExecutor e) throws OrmException {
delegate.pruneSchema(e);
}
@Override
public Access<?, ?>[] allRelations() {
return delegate.allRelations();
}
@Override
public void close() {
delegate.close();
}
@Override
public SchemaVersionAccess schemaVersion() {
return delegate.schemaVersion();
}
@Override
public SystemConfigAccess systemConfig() {
return delegate.systemConfig();
}
@Override
public AccountAccess accounts() {
return delegate.accounts();
}
@Override
public AccountExternalIdAccess accountExternalIds() {
return delegate.accountExternalIds();
}
@Override
public AccountSshKeyAccess accountSshKeys() {
return delegate.accountSshKeys();
}
@Override
public AccountGroupAccess accountGroups() {
return delegate.accountGroups();
}
@Override
public AccountGroupNameAccess accountGroupNames() {
return delegate.accountGroupNames();
}
@Override
public AccountGroupMemberAccess accountGroupMembers() {
return delegate.accountGroupMembers();
}
@Override
public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
return delegate.accountGroupMembersAudit();
}
@Override
public StarredChangeAccess starredChanges() {
return delegate.starredChanges();
}
@Override
public AccountProjectWatchAccess accountProjectWatches() {
return delegate.accountProjectWatches();
}
@Override
public AccountPatchReviewAccess accountPatchReviews() {
return delegate.accountPatchReviews();
}
@Override
public ChangeAccess changes() {
return delegate.changes();
}
@Override
public PatchSetApprovalAccess patchSetApprovals() {
return delegate.patchSetApprovals();
}
@Override
public ChangeMessageAccess changeMessages() {
return delegate.changeMessages();
}
@Override
public PatchSetAccess patchSets() {
return delegate.patchSets();
}
@Override
public PatchLineCommentAccess patchComments() {
return delegate.patchComments();
}
@Override
public SubmoduleSubscriptionAccess submoduleSubscriptions() {
return delegate.submoduleSubscriptions();
}
@Override
public AccountGroupByIdAccess accountGroupById() {
return delegate.accountGroupById();
}
@Override
public AccountGroupByIdAudAccess accountGroupByIdAud() {
return delegate.accountGroupByIdAud();
}
@Override
public int nextAccountId() throws OrmException {
return delegate.nextAccountId();
}
@Override
public int nextAccountGroupId() throws OrmException {
return delegate.nextAccountGroupId();
}
@Override
@SuppressWarnings("deprecation")
public int nextChangeId() throws OrmException {
return delegate.nextChangeId();
}
@Override
public int nextChangeMessageId() throws OrmException {
return delegate.nextChangeMessageId();
}
public static class ChangeAccessWrapper implements ChangeAccess {
protected final ChangeAccess delegate;
protected ChangeAccessWrapper(ChangeAccess delegate) {
this.delegate = checkNotNull(delegate);
}
@Override
public String getRelationName() {
return delegate.getRelationName();
}
@Override
public int getRelationID() {
return delegate.getRelationID();
}
@Override
public ResultSet<Change> iterateAllEntities() throws OrmException {
return delegate.iterateAllEntities();
}
@Override
public Change.Id primaryKey(Change entity) {
return delegate.primaryKey(entity);
}
@Override
public Map<Change.Id, Change> toMap(Iterable<Change> c) {
return delegate.toMap(c);
}
@Override
public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
return delegate.getAsync(key);
}
@Override
public ResultSet<Change> get(Iterable<Change.Id> keys) throws OrmException {
return delegate.get(keys);
}
@Override
public void insert(Iterable<Change> instances) throws OrmException {
delegate.insert(instances);
}
@Override
public void update(Iterable<Change> instances) throws OrmException {
delegate.update(instances);
}
@Override
public void upsert(Iterable<Change> instances) throws OrmException {
delegate.upsert(instances);
}
@Override
public void deleteKeys(Iterable<Change.Id> keys) throws OrmException {
delegate.deleteKeys(keys);
}
@Override
public void delete(Iterable<Change> instances) throws OrmException {
delegate.delete(instances);
}
@Override
public void beginTransaction(Change.Id key) throws OrmException {
delegate.beginTransaction(key);
}
@Override
public Change atomicUpdate(Change.Id key, AtomicUpdate<Change> update)
throws OrmException {
return delegate.atomicUpdate(key, update);
}
@Override
public Change get(Change.Id id) throws OrmException {
return delegate.get(id);
}
@Override
public ResultSet<Change> all() throws OrmException {
return delegate.all();
}
}
}