Java tutorial
// 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.server.notedb; 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 static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; 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 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; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; /** * The state of all relevant NoteDb refs across all repos corresponding to a given Change entity. * * <p>Stored serialized in the {@code Change#noteDbState} field, and used to determine whether the * state in NoteDb is out of date. * * <p>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=[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. */ public class NoteDbChangeState { public static final String NOTE_DB_PRIMARY_STATE = "N"; public enum PrimaryStorage { REVIEW_DB('R'), NOTE_DB('N'); private final char code; PrimaryStorage(char code) { this.code = code; } public static PrimaryStorage of(@Nullable Change c) { return of(NoteDbChangeState.parse(c)); } public static PrimaryStorage of(@Nullable NoteDbChangeState s) { return s != null ? s.getPrimaryStorage() : REVIEW_DB; } } @AutoValue public abstract static class Delta { @VisibleForTesting public static Delta create(Change.Id changeId, Optional<ObjectId> newChangeMetaId, Map<Account.Id, ObjectId> newDraftIds) { if (newDraftIds == null) { newDraftIds = ImmutableMap.of(); } return new AutoValue_NoteDbChangeState_Delta(changeId, newChangeMetaId, ImmutableMap.copyOf(newDraftIds)); } abstract Change.Id changeId(); abstract Optional<ObjectId> newChangeMetaId(); abstract ImmutableMap<Account.Id, ObjectId> newDraftIds(); } @AutoValue public abstract static class RefState { @VisibleForTesting public static RefState create(ObjectId changeMetaId, Map<Account.Id, ObjectId> draftIds) { return new AutoValue_NoteDbChangeState_RefState(changeMetaId.copy(), ImmutableMap.copyOf(Maps.filterValues(draftIds, id -> !ObjectId.zeroId().equals(id)))); } private static Optional<RefState> parse(Change.Id changeId, List<String> parts) { checkArgument(!parts.isEmpty(), "missing state string for change %s", changeId); ObjectId changeMetaId = ObjectId.fromString(parts.get(0)); Map<Account.Id, ObjectId> draftIds = Maps.newHashMapWithExpectedSize(parts.size() - 1); Splitter s = Splitter.on('='); for (int i = 1; i < parts.size(); i++) { String p = parts.get(i); List<String> draftParts = s.splitToList(p); checkArgument(draftParts.size() == 2, "invalid draft state part for change %s: %s", changeId, p); draftIds.put(Account.Id.parse(draftParts.get(0)), ObjectId.fromString(draftParts.get(1))); } return Optional.of(create(changeMetaId, draftIds)); } abstract ObjectId changeMetaId(); abstract ImmutableMap<Account.Id, ObjectId> draftIds(); @Override public String toString() { return appendTo(new StringBuilder()).toString(); } StringBuilder appendTo(StringBuilder sb) { sb.append(changeMetaId().name()); for (Account.Id id : ReviewDbUtil.intKeyOrdering().sortedCopy(draftIds().keySet())) { sb.append(',').append(id.get()).append('=').append(draftIds().get(id).name()); } return sb; } } public static NoteDbChangeState parse(@Nullable Change c) { return c != null ? parse(c.getId(), c.getNoteDbState()) : null; } @VisibleForTesting public static NoteDbChangeState parse(Change.Id id, @Nullable String str) { if (Strings.isNullOrEmpty(str)) { // Return null rather than Optional as this is what goes in the field in // ReviewDb. 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". if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) { return new NoteDbChangeState(id, NOTE_DB, Optional.empty(), readOnlyUntil); } // Otherwise it must be REVIEW_DB, either "R,<RefState>" or just // "<RefState>". Allow length > 0 for forward compatibility. if (first.length() > 0) { Optional<RefState> refState; if (first.charAt(0) == REVIEW_DB.code) { refState = RefState.parse(id, parts.subList(1, parts.size())); } else { refState = RefState.parse(id, parts); } return new NoteDbChangeState(id, REVIEW_DB, refState, readOnlyUntil); } 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; } String oldStr = change.getNoteDbState(); if (oldStr == null && !delta.newChangeMetaId().isPresent()) { // Neither an old nor a new meta ID was present, most likely because we // aren't writing a NoteDb graph at all for this change at this point. No // point in proceeding. return null; } NoteDbChangeState oldState = parse(change.getId(), oldStr); if (oldState != null && oldState.getPrimaryStorage() == NOTE_DB) { // NOTE_DB state doesn't include RefState, so applying a delta is a no-op. return oldState; } ObjectId changeMetaId; if (delta.newChangeMetaId().isPresent()) { changeMetaId = delta.newChangeMetaId().get(); if (changeMetaId.equals(ObjectId.zeroId())) { change.setNoteDbState(null); return null; } } else { changeMetaId = oldState.getChangeMetaId(); } Map<Account.Id, ObjectId> draftIds = new HashMap<>(); if (oldState != null) { draftIds.putAll(oldState.getDraftIds()); } for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) { if (e.getValue().equals(ObjectId.zeroId())) { draftIds.remove(e.getKey()); } else { draftIds.put(e.getKey(), e.getValue()); } } NoteDbChangeState state = new NoteDbChangeState(change.getId(), oldState != null ? oldState.getPrimaryStorage() : REVIEW_DB, 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) { return true; // Primary storage is NoteDb, up to date by definition. } if (state == null) { return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent(); } return state.isChangeUpToDate(changeRepoRefs); } public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state, RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId) throws IOException { if (PrimaryStorage.of(state) == NOTE_DB) { return true; // Primary storage is NoteDb, up to date by definition. } if (state == null) { return !draftsRepoRefs.get(refsDraftComments(changeId, accountId)).isPresent(); } return state.areDraftsUpToDate(draftsRepoRefs, accountId); } public static long getReadOnlySkew(Config cfg) { return cfg.getTimeUnit("notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS); } 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<Timestamp> readOnlyUntil) { this.changeId = checkNotNull(changeId); this.primaryStorage = checkNotNull(primaryStorage); this.refState = checkNotNull(refState); this.readOnlyUntil = checkNotNull(readOnlyUntil); switch (primaryStorage) { case REVIEW_DB: checkArgument(refState.isPresent(), "expected RefState for change %s with primary storage %s", changeId, primaryStorage); break; case NOTE_DB: checkArgument(!refState.isPresent(), "expected no RefState for change %s with primary storage %s", changeId, primaryStorage); break; default: throw new IllegalStateException("invalid PrimaryStorage: " + primaryStorage); } } public PrimaryStorage getPrimaryStorage() { return primaryStorage; } public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException { if (primaryStorage == NOTE_DB) { return true; // Primary storage is NoteDb, up to date by definition. } Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId)); if (!id.isPresent()) { return getChangeMetaId().equals(ObjectId.zeroId()); } return id.get().equals(getChangeMetaId()); } public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId) throws IOException { if (primaryStorage == NOTE_DB) { return true; // Primary storage is NoteDb, up to date by definition. } Optional<ObjectId> id = draftsRepoRefs.get(refsDraftComments(changeId, accountId)); if (!id.isPresent()) { return !getDraftIds().containsKey(accountId); } return id.get().equals(getDraftIds().get(accountId)); } public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) throws IOException { if (primaryStorage == NOTE_DB) { return true; // Primary storage is NoteDb, up to date by definition. } if (!isChangeUpToDate(changeRepoRefs)) { return false; } for (Account.Id accountId : getDraftIds().keySet()) { if (!areDraftsUpToDate(draftsRepoRefs, accountId)) { return false; } } return true; } 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; } public ObjectId getChangeMetaId() { return refState().changeMetaId(); } public ImmutableMap<Account.Id, ObjectId> getDraftIds() { return refState().draftIds(); } public Optional<RefState> getRefState() { return refState; } private RefState refState() { checkState(refState.isPresent(), "state for %s has no RefState: %s", changeId, this); return refState.get(); } @Override public String toString() { switch (primaryStorage) { case REVIEW_DB: 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: 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); } }