Merge "Support temporarily read-only changes via NoteDbChangeState"
This commit is contained in:
@@ -49,10 +49,12 @@ import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.GerritPersonIdent;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.config.AllUsersName;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
||||
import com.google.gerrit.server.index.change.ChangeIndexer;
|
||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState;
|
||||
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
|
||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
|
||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException;
|
||||
@@ -71,6 +73,7 @@ import com.google.inject.assistedinject.Assisted;
|
||||
import com.google.inject.assistedinject.AssistedInject;
|
||||
|
||||
import org.eclipse.jgit.lib.BatchRefUpdate;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.NullProgressMonitor;
|
||||
import org.eclipse.jgit.lib.ObjectInserter;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
@@ -508,6 +511,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
private final NotesMigration notesMigration;
|
||||
private final ReviewDb db;
|
||||
private final SchemaFactory<ReviewDb> schemaFactory;
|
||||
private final long skewMs;
|
||||
|
||||
private final Project.NameKey project;
|
||||
private final CurrentUser user;
|
||||
@@ -533,6 +537,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
|
||||
@AssistedInject
|
||||
BatchUpdate(
|
||||
@GerritServerConfig Config cfg,
|
||||
AllUsersName allUsers,
|
||||
ChangeControl.GenericFactory changeControlFactory,
|
||||
ChangeIndexer indexer,
|
||||
@@ -568,6 +573,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
this.when = when;
|
||||
tz = serverIdent.getTimeZone();
|
||||
order = Order.REPO_BEFORE_DB;
|
||||
skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -942,7 +948,10 @@ public class BatchUpdate implements AutoCloseable {
|
||||
db.changes().beginTransaction(id);
|
||||
try {
|
||||
ChangeContext ctx = newChangeContext(db, repo, rw, id);
|
||||
storage = PrimaryStorage.of(ctx.getChange());
|
||||
NoteDbChangeState oldState = NoteDbChangeState.parse(ctx.getChange());
|
||||
NoteDbChangeState.checkNotReadOnly(oldState, skewMs);
|
||||
|
||||
storage = PrimaryStorage.of(oldState);
|
||||
if (storage == PrimaryStorage.NOTE_DB
|
||||
&& !notesMigration.readChanges()) {
|
||||
throw new OrmException(
|
||||
@@ -1037,6 +1046,7 @@ public class BatchUpdate implements AutoCloseable {
|
||||
logDebug("Failed to get change {} from unwrapped db", id);
|
||||
throw new NoSuchChangeException(id);
|
||||
}
|
||||
NoteDbChangeState.checkNotReadOnly(c, skewMs);
|
||||
}
|
||||
// Pass in preloaded change to controlFor, to avoid:
|
||||
// - reading from a db that does not belong to this update
|
||||
|
||||
@@ -28,19 +28,26 @@ import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
||||
import com.google.gerrit.server.git.RefCache;
|
||||
import com.google.gwtorm.server.OrmRuntimeException;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The state of all relevant NoteDb refs across all repos corresponding to a
|
||||
@@ -52,8 +59,10 @@ import java.util.Optional;
|
||||
* Serialized in one of the forms:
|
||||
* <ul>
|
||||
* <li>[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
|
||||
* <li>R[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
|
||||
* <li>R,[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
|
||||
* <li>R=[read-only-until],[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
|
||||
* <li>N
|
||||
* <li>N=[read-only-until]
|
||||
* </ul>
|
||||
*
|
||||
* in numeric account ID order, with hex SHA-1s for human readability.
|
||||
@@ -163,11 +172,13 @@ public class NoteDbChangeState {
|
||||
return null;
|
||||
}
|
||||
List<String> parts = Splitter.on(',').splitToList(str);
|
||||
String first = parts.get(0);
|
||||
Optional<Timestamp> readOnlyUntil = parseReadOnlyUntil(id, str, first);
|
||||
|
||||
// Only valid NOTE_DB state is "N".
|
||||
String first = parts.get(0);
|
||||
if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) {
|
||||
return new NoteDbChangeState(id, NOTE_DB, Optional.empty());
|
||||
return new NoteDbChangeState(
|
||||
id, NOTE_DB, Optional.empty(), readOnlyUntil);
|
||||
}
|
||||
|
||||
// Otherwise it must be REVIEW_DB, either "R,<RefState>" or just
|
||||
@@ -179,12 +190,43 @@ public class NoteDbChangeState {
|
||||
} else {
|
||||
refState = RefState.parse(id, parts);
|
||||
}
|
||||
return new NoteDbChangeState(id, REVIEW_DB, refState);
|
||||
return new NoteDbChangeState(id, REVIEW_DB, refState, readOnlyUntil);
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
throw invalidState(id, str);
|
||||
}
|
||||
|
||||
private static Optional<Timestamp> parseReadOnlyUntil(Change.Id id,
|
||||
String fullStr, String first) {
|
||||
if (first.length() > 2 && first.charAt(1) == '=') {
|
||||
Long ts = Longs.tryParse(first.substring(2));
|
||||
if (ts == null) {
|
||||
throw invalidState(id, fullStr);
|
||||
}
|
||||
return Optional.of(new Timestamp(ts));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static IllegalArgumentException invalidState(Change.Id id,
|
||||
String str) {
|
||||
return new IllegalArgumentException(
|
||||
"invalid state string for change " + id + ": " + str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a delta to the state stored in a change entity.
|
||||
* <p>
|
||||
* This method does not check whether the old state was read-only; it is up to
|
||||
* the caller to not violate read-only semantics when storing the change back
|
||||
* in ReviewDb.
|
||||
*
|
||||
* @param change change entity. The delta is applied against this entity's
|
||||
* {@code noteDbState} and the new state is stored back in the entity as a
|
||||
* side effect.
|
||||
* @param delta delta to apply.
|
||||
* @return new state, equivalent to what is stored in {@code change} as a side
|
||||
* effect.
|
||||
*/
|
||||
public static NoteDbChangeState applyDelta(Change change, Delta delta) {
|
||||
if (delta == null) {
|
||||
return null;
|
||||
@@ -230,11 +272,21 @@ public class NoteDbChangeState {
|
||||
oldState != null
|
||||
? oldState.getPrimaryStorage()
|
||||
: REVIEW_DB,
|
||||
Optional.of(RefState.create(changeMetaId, draftIds)));
|
||||
Optional.of(RefState.create(changeMetaId, draftIds)),
|
||||
// Copy old read-only deadline rather than advancing it; the caller is
|
||||
// still responsible for finishing the rest of its work before the lease
|
||||
// runs out.
|
||||
oldState != null ? oldState.getReadOnlyUntil() : Optional.empty());
|
||||
change.setNoteDbState(state.toString());
|
||||
return state;
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Ugly. Refactor these static methods into a Checker class
|
||||
// or something. They do not belong in NoteDbChangeState itself because:
|
||||
// - need to inject Config but don't want a whole Factory
|
||||
// - can't be methods on NoteDbChangeState because state is nullable (though
|
||||
// we could also solve this by inventing an empty-but-non-null state)
|
||||
// Also we should clean up duplicated code between static/non-static methods.
|
||||
public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state,
|
||||
RefCache changeRepoRefs, Change.Id changeId) throws IOException {
|
||||
if (PrimaryStorage.of(state) == NOTE_DB) {
|
||||
@@ -259,17 +311,46 @@ public class NoteDbChangeState {
|
||||
return state.areDraftsUpToDate(draftsRepoRefs, accountId);
|
||||
}
|
||||
|
||||
public static long getReadOnlySkew(Config cfg) {
|
||||
return cfg.getTimeUnit(
|
||||
"notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private static Timestamp timeForReadOnlyCheck(long skewMs) {
|
||||
// Subtract some slop in case the machine that set the change's read-only
|
||||
// lease has a clock behind ours.
|
||||
return new Timestamp(TimeUtil.nowMs() - skewMs);
|
||||
}
|
||||
|
||||
public static void checkNotReadOnly(@Nullable Change change, long skewMs) {
|
||||
checkNotReadOnly(parse(change), skewMs);
|
||||
}
|
||||
|
||||
public static void checkNotReadOnly(@Nullable NoteDbChangeState state,
|
||||
long skewMs) {
|
||||
if (state == null) {
|
||||
return; // No state means ReviewDb primary non-read-only.
|
||||
} else if (state.isReadOnly(timeForReadOnlyCheck(skewMs))) {
|
||||
throw new OrmRuntimeException(
|
||||
"change " + state.getChangeId() + " is read-only until "
|
||||
+ state.getReadOnlyUntil().get());
|
||||
}
|
||||
}
|
||||
|
||||
private final Change.Id changeId;
|
||||
private final PrimaryStorage primaryStorage;
|
||||
private final Optional<RefState> refState;
|
||||
private final Optional<Timestamp> readOnlyUntil;
|
||||
|
||||
public NoteDbChangeState(
|
||||
Change.Id changeId,
|
||||
PrimaryStorage primaryStorage,
|
||||
Optional<RefState> refState) {
|
||||
Optional<RefState> refState,
|
||||
Optional<Timestamp> readOnlyUntil) {
|
||||
this.changeId = checkNotNull(changeId);
|
||||
this.primaryStorage = checkNotNull(primaryStorage);
|
||||
this.refState = refState;
|
||||
this.refState = checkNotNull(refState);
|
||||
this.readOnlyUntil = checkNotNull(readOnlyUntil);
|
||||
|
||||
switch (primaryStorage) {
|
||||
case REVIEW_DB:
|
||||
@@ -334,23 +415,32 @@ public class NoteDbChangeState {
|
||||
return true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Change.Id getChangeId() {
|
||||
public boolean isReadOnly(Timestamp now) {
|
||||
return readOnlyUntil.isPresent() && now.before(readOnlyUntil.get());
|
||||
}
|
||||
|
||||
public Optional<Timestamp> getReadOnlyUntil() {
|
||||
return readOnlyUntil;
|
||||
}
|
||||
|
||||
public NoteDbChangeState withReadOnlyUntil(Timestamp ts) {
|
||||
return new NoteDbChangeState(
|
||||
changeId, primaryStorage, refState, Optional.of(ts));
|
||||
}
|
||||
|
||||
public Change.Id getChangeId() {
|
||||
return changeId;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ObjectId getChangeMetaId() {
|
||||
return refState().changeMetaId();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ImmutableMap<Account.Id, ObjectId> getDraftIds() {
|
||||
public ImmutableMap<Account.Id, ObjectId> getDraftIds() {
|
||||
return refState().draftIds();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Optional<RefState> getRefState() {
|
||||
public Optional<RefState> getRefState() {
|
||||
return refState;
|
||||
}
|
||||
|
||||
@@ -364,13 +454,37 @@ public class NoteDbChangeState {
|
||||
public String toString() {
|
||||
switch (primaryStorage) {
|
||||
case REVIEW_DB:
|
||||
// Don't include enum field, just IDs (though parse would accept it).
|
||||
return refState().toString();
|
||||
if (!readOnlyUntil.isPresent()) {
|
||||
// Don't include enum field, just IDs (though parse would accept it).
|
||||
return refState().toString();
|
||||
}
|
||||
return primaryStorage.code + "=" + readOnlyUntil.get().getTime()
|
||||
+ "," + refState.get();
|
||||
case NOTE_DB:
|
||||
return NOTE_DB_PRIMARY_STATE;
|
||||
if (!readOnlyUntil.isPresent()) {
|
||||
return NOTE_DB_PRIMARY_STATE;
|
||||
}
|
||||
return primaryStorage.code + "=" + readOnlyUntil.get().getTime();
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported PrimaryStorage: " + primaryStorage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(changeId, primaryStorage, refState, readOnlyUntil);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof NoteDbChangeState)) {
|
||||
return false;
|
||||
}
|
||||
NoteDbChangeState s = (NoteDbChangeState) o;
|
||||
return changeId.equals(s.changeId)
|
||||
&& primaryStorage.equals(s.primaryStorage)
|
||||
&& refState.equals(s.refState)
|
||||
&& readOnlyUntil.equals(s.readOnlyUntil);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.gerrit.extensions.config.FactoryModule;
|
||||
import com.google.gerrit.reviewdb.client.Change;
|
||||
import com.google.gerrit.reviewdb.client.Change.Id;
|
||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
|
||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
|
||||
@@ -68,6 +69,11 @@ public class NoteDbModule extends FactoryModule {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result rebuildEvenIfReadOnly(ReviewDb db, Id changeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result rebuild(NoteDbUpdateManager manager,
|
||||
ChangeBundle bundle) {
|
||||
|
||||
@@ -55,10 +55,23 @@ public class TestChangeRebuilderWrapper extends ChangeRebuilder {
|
||||
@Override
|
||||
public Result rebuild(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException {
|
||||
return rebuild(db, changeId, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException {
|
||||
return rebuild(db, changeId, false);
|
||||
}
|
||||
|
||||
private Result rebuild(ReviewDb db, Change.Id changeId,
|
||||
boolean checkReadOnly) throws IOException, OrmException {
|
||||
if (failNextUpdate.getAndSet(false)) {
|
||||
throw new IOException("Update failed");
|
||||
}
|
||||
Result result = delegate.rebuild(db, changeId);
|
||||
Result result = checkReadOnly
|
||||
? delegate.rebuild(db, changeId)
|
||||
: delegate.rebuildEvenIfReadOnly(db, changeId);
|
||||
if (stealNextUpdate.getAndSet(false)) {
|
||||
throw new IOException("Update stolen");
|
||||
}
|
||||
|
||||
@@ -58,6 +58,9 @@ public abstract class ChangeRebuilder {
|
||||
public abstract Result rebuild(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException;
|
||||
|
||||
public abstract Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException;
|
||||
|
||||
public abstract Result rebuild(NoteDbUpdateManager manager,
|
||||
ChangeBundle bundle) throws IOException, OrmException;
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import com.google.gerrit.server.CommentsUtil;
|
||||
import com.google.gerrit.server.GerritPersonIdent;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.config.AnonymousCowardName;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.config.GerritServerId;
|
||||
import com.google.gerrit.server.git.ChainedReceiveCommands;
|
||||
import com.google.gerrit.server.notedb.ChangeBundle;
|
||||
@@ -73,6 +74,7 @@ import com.google.gwtorm.server.SchemaFactory;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
@@ -121,9 +123,11 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
private final ProjectCache projectCache;
|
||||
private final String anonymousCowardName;
|
||||
private final String serverId;
|
||||
private final long skewMs;
|
||||
|
||||
@Inject
|
||||
ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
|
||||
ChangeRebuilderImpl(@GerritServerConfig Config cfg,
|
||||
SchemaFactory<ReviewDb> schemaFactory,
|
||||
AccountCache accountCache,
|
||||
ChangeBundleReader bundleReader,
|
||||
ChangeDraftUpdate.Factory draftUpdateFactory,
|
||||
@@ -149,11 +153,23 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
this.projectCache = projectCache;
|
||||
this.anonymousCowardName = anonymousCowardName;
|
||||
this.serverId = serverId;
|
||||
this.skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result rebuild(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException {
|
||||
return rebuild(db, changeId, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
|
||||
throws IOException, OrmException {
|
||||
return rebuild(db, changeId, false);
|
||||
}
|
||||
|
||||
private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly)
|
||||
throws IOException, OrmException {
|
||||
db = ReviewDbUtil.unwrapDb(db);
|
||||
// Read change just to get project; this instance is then discarded so we
|
||||
// can read a consistent ChangeBundle inside a transaction.
|
||||
@@ -164,7 +180,7 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
try (NoteDbUpdateManager manager =
|
||||
updateManagerFactory.create(change.getProject())) {
|
||||
buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
|
||||
return execute(db, changeId, manager);
|
||||
return execute(db, changeId, manager, checkReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +212,12 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
@Override
|
||||
public Result execute(ReviewDb db, Change.Id changeId,
|
||||
NoteDbUpdateManager manager) throws OrmException, IOException {
|
||||
return execute(db, changeId, manager, true);
|
||||
}
|
||||
|
||||
public Result execute(ReviewDb db, Change.Id changeId,
|
||||
NoteDbUpdateManager manager, boolean checkReadOnly)
|
||||
throws OrmException, IOException {
|
||||
db = ReviewDbUtil.unwrapDb(db);
|
||||
Change change =
|
||||
checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId));
|
||||
@@ -210,6 +232,9 @@ public class ChangeRebuilderImpl extends ChangeRebuilder {
|
||||
db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
|
||||
@Override
|
||||
public Change update(Change change) {
|
||||
if (checkReadOnly) {
|
||||
NoteDbChangeState.checkNotReadOnly(change, skewMs);
|
||||
}
|
||||
String currNoteDbState = change.getNoteDbState();
|
||||
if (Objects.equals(currNoteDbState, newNoteDbState)) {
|
||||
// Another thread completed the same rebuild we were about to.
|
||||
|
||||
Reference in New Issue
Block a user