blob: e560ec8a000bdc8923e90b81e388d4714cb52cd9 [file] [log] [blame]
// Copyright (C) 2017 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.gerrit.server.notedb.NoteDbTable.CHANGES;
import com.google.auto.value.AutoValue;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.inject.AbstractModule;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.lib.Config;
/**
* Current low-level settings of the NoteDb migration for changes.
*
* <p>This class only describes the migration state of the {@link
* com.google.gerrit.reviewdb.client.Change Change} entity group, since it is possible for a given
* site to be in different states of the Change NoteDb migration process while staying at the same
* ReviewDb schema version. It does <em>not</em> describe the migration state of non-Change tables;
* those are automatically migrated using the ReviewDb schema migration process, so the NoteDb
* migration state at a given ReviewDb schema cannot vary.
*
* <p>In many places, core Gerrit code should not directly care about the NoteDb migration state,
* and should prefer high-level APIs like {@link com.google.gerrit.server.ApprovalsUtil
* ApprovalsUtil} that don't require callers to inspect the migration state. The
* <em>implementation</em> of those utilities does care about the state, and should query the {@code
* NotesMigration} for the properties of the migration, for example, {@link #changePrimaryStorage()
* where new changes should be stored}.
*
* <p>Core Gerrit code is mostly interested in one facet of the migration at a time (reading or
* writing, say), but not all combinations of return values are supported or even make sense.
*
* <p>This class controls the state of the migration according to options in {@code gerrit.config}.
* In general, any changes to these options should only be made by adventurous administrators, who
* know what they're doing, on non-production data, for the purposes of testing the NoteDb
* implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For
* these reasons, the options remain undocumented.
*
* <p><strong>Note:</strong> Callers should not assume the values returned by {@code
* NotesMigration}'s methods will not change in a running server.
*/
public abstract class NotesMigration {
public static final String SECTION_NOTE_DB = "noteDb";
private static final String DISABLE_REVIEW_DB = "disableReviewDb";
private static final String PRIMARY_STORAGE = "primaryStorage";
private static final String READ = "read";
private static final String SEQUENCE = "sequence";
private static final String WRITE = "write";
public static class Module extends AbstractModule {
@Override
public void configure() {
bind(MutableNotesMigration.class);
bind(NotesMigration.class).to(MutableNotesMigration.class);
}
}
@AutoValue
abstract static class Snapshot {
static Builder builder() {
// Default values are defined as what we would read from an empty config.
return create(new Config()).toBuilder();
}
static Snapshot create(Config cfg) {
return new AutoValue_NotesMigration_Snapshot.Builder()
.setWriteChanges(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, false))
.setReadChanges(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, false))
.setReadChangeSequence(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, false))
.setChangePrimaryStorage(
cfg.getEnum(
SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB))
.setDisableChangeReviewDb(
cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false))
.setFailOnLoadForTest(false) // Only set in tests, can't be set via config.
.build();
}
abstract boolean writeChanges();
abstract boolean readChanges();
abstract boolean readChangeSequence();
abstract PrimaryStorage changePrimaryStorage();
abstract boolean disableChangeReviewDb();
abstract boolean failOnLoadForTest();
abstract Builder toBuilder();
void setConfigValues(Config cfg) {
cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, writeChanges());
cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, readChanges());
cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, readChangeSequence());
cfg.setEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, changePrimaryStorage());
cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, disableChangeReviewDb());
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder setWriteChanges(boolean writeChanges);
abstract Builder setReadChanges(boolean readChanges);
abstract Builder setReadChangeSequence(boolean readChangeSequence);
abstract Builder setChangePrimaryStorage(PrimaryStorage changePrimaryStorage);
abstract Builder setDisableChangeReviewDb(boolean disableChangeReviewDb);
abstract Builder setFailOnLoadForTest(boolean failOnLoadForTest);
abstract Snapshot autoBuild();
Snapshot build() {
Snapshot s = autoBuild();
checkArgument(
!(s.disableChangeReviewDb() && s.changePrimaryStorage() != PrimaryStorage.NOTE_DB),
"cannot disable ReviewDb for changes if default change primary storage is ReviewDb");
return s;
}
}
}
protected final AtomicReference<Snapshot> snapshot;
/**
* Read changes from NoteDb.
*
* <p>Change data is read from NoteDb refs, but ReviewDb is still the source of truth. If the
* loader determines NoteDb is out of date, the change data in NoteDb will be transparently
* rebuilt. This means that some code paths that look read-only may in fact attempt to write.
*
* <p>If true and {@code writeChanges() = false}, changes can still be read from NoteDb, but any
* attempts to write will generate an error.
*/
public final boolean readChanges() {
return snapshot.get().readChanges();
}
/**
* Write changes to NoteDb.
*
* <p>This method is awkwardly named because you should be using either {@link
* #commitChangeWrites()} or {@link #failChangeWrites()} instead.
*
* <p>Updates to change data are written to NoteDb refs, but ReviewDb is still the source of
* truth. Change data will not be written unless the NoteDb refs are already up to date, and the
* write path will attempt to rebuild the change if not.
*
* <p>If false, the behavior when attempting to write depends on {@code readChanges()}. If {@code
* readChanges() = false}, writes to NoteDb are simply ignored; if {@code true}, any attempts to
* write will generate an error.
*/
public final boolean rawWriteChangesSetting() {
return snapshot.get().writeChanges();
}
/**
* Read sequential change ID numbers from NoteDb.
*
* <p>If true, change IDs are read from {@code refs/sequences/changes} in All-Projects. If false,
* change IDs are read from ReviewDb's native sequences.
*/
public final boolean readChangeSequence() {
return snapshot.get().readChangeSequence();
}
/** @return default primary storage for new changes. */
public final PrimaryStorage changePrimaryStorage() {
return snapshot.get().changePrimaryStorage();
}
/**
* Disable ReviewDb access for changes.
*
* <p>When set, ReviewDb operations involving the Changes table become no-ops. Lookups return no
* results; updates do nothing, as does opening, committing, or rolling back a transaction on the
* Changes table.
*/
public final boolean disableChangeReviewDb() {
return snapshot.get().disableChangeReviewDb();
}
/**
* Whether to fail when reading any data from NoteDb.
*
* <p>Used in conjunction with {@link #readChanges()} for tests.
*/
public boolean failOnLoadForTest() {
return snapshot.get().failOnLoadForTest();
}
public final boolean commitChangeWrites() {
// It may seem odd that readChanges() without writeChanges() means we should
// attempt to commit writes. However, this method is used by callers to know
// whether or not they should short-circuit and skip attempting to read or
// write NoteDb refs.
//
// It is possible for commitChangeWrites() to return true and
// failChangeWrites() to also return true, causing an error later in the
// same codepath. This specific condition is used by the auto-rebuilding
// path to rebuild a change and stage the results, but not commit them due
// to failChangeWrites().
return rawWriteChangesSetting() || readChanges();
}
public final boolean failChangeWrites() {
return !rawWriteChangesSetting() && readChanges();
}
public final void setConfigValues(Config cfg) {
snapshot.get().setConfigValues(cfg);
}
@Override
public final boolean equals(Object o) {
return o instanceof NotesMigration
&& snapshot.get().equals(((NotesMigration) o).snapshot.get());
}
@Override
public final int hashCode() {
return snapshot.get().hashCode();
}
protected NotesMigration(Snapshot snapshot) {
this.snapshot = new AtomicReference<>(snapshot);
}
final Snapshot snapshot() {
return snapshot.get();
}
}