All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.notedb.NoteDbChangeState Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.
 *
 * 

Stored serialized in the {@code Change#noteDbState} field, and used to determine whether the * state in NoteDb is out of date. * *

Serialized in one of the forms: * *

    *
  • [meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... *
  • R,[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... *
  • R=[read-only-until],[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... *
  • N *
  • N=[read-only-until] *
* * 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(Change c) { return of(NoteDbChangeState.parse(c)); } public static PrimaryStorage of(NoteDbChangeState s) { return s != null ? s.getPrimaryStorage() : REVIEW_DB; } } @AutoValue public abstract static class Delta { @VisibleForTesting public static Delta create( Change.Id changeId, Optional newChangeMetaId, Map newDraftIds) { if (newDraftIds == null) { newDraftIds = ImmutableMap.of(); } return new AutoValue_NoteDbChangeState_Delta( changeId, newChangeMetaId, ImmutableMap.copyOf(newDraftIds)); } abstract Change.Id changeId(); abstract Optional newChangeMetaId(); abstract ImmutableMap newDraftIds(); } @AutoValue public abstract static class RefState { @VisibleForTesting public static RefState create(ObjectId changeMetaId, Map draftIds) { return new AutoValue_NoteDbChangeState_RefState( changeMetaId.copy(), ImmutableMap.copyOf(Maps.filterValues(draftIds, id -> !ObjectId.zeroId().equals(id)))); } private static Optional parse(Change.Id changeId, List parts) { checkArgument(!parts.isEmpty(), "missing state string for change %s", changeId); ObjectId changeMetaId = ObjectId.fromString(parts.get(0)); Map draftIds = Maps.newHashMapWithExpectedSize(parts.size() - 1); Splitter s = Splitter.on('='); for (int i = 1; i < parts.size(); i++) { String p = parts.get(i); List 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 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(Change c) { return c != null ? parse(c.getId(), c.getNoteDbState()) : null; } @VisibleForTesting public static NoteDbChangeState parse(Change.Id id, String str) { if (Strings.isNullOrEmpty(str)) { // Return null rather than Optional as this is what goes in the field in // ReviewDb. return null; } List parts = Splitter.on(',').splitToList(str); String first = parts.get(0); Optional 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," or just // "". Allow length > 0 for forward compatibility. if (first.length() > 0) { Optional 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 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. * *

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 draftIds = new HashMap<>(); if (oldState != null) { draftIds.putAll(oldState.getDraftIds()); } for (Map.Entry 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; private final Optional readOnlyUntil; public NoteDbChangeState( Change.Id changeId, PrimaryStorage primaryStorage, Optional refState, Optional 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 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 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 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 getDraftIds() { return refState().draftIds(); } public Optional 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy