| // Copyright (C) 2014 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 java.util.Objects.requireNonNull; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.UsedAt; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.metrics.Timer0; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.config.GerritServerId; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** View of contents at a single ref related to some change. * */ |
| public abstract class AbstractChangeNotes<T> { |
| @VisibleForTesting |
| @Singleton |
| @UsedAt(UsedAt.Project.PLUGIN_CHECKS) |
| public static class Args { |
| // TODO(dborowitz): Some less smelly way of disabling NoteDb in tests. |
| public final AtomicBoolean failOnLoadForTest; |
| public final ChangeNoteJson changeNoteJson; |
| public final GitRepositoryManager repoManager; |
| public final AllUsersName allUsers; |
| public final NoteDbMetrics metrics; |
| public final String serverId; |
| |
| // Providers required to avoid dependency cycles. |
| |
| // ChangeNoteCache -> Args |
| public final Provider<ChangeNotesCache> cache; |
| |
| @Inject |
| Args( |
| GitRepositoryManager repoManager, |
| AllUsersName allUsers, |
| ChangeNoteJson changeNoteJson, |
| NoteDbMetrics metrics, |
| Provider<ChangeNotesCache> cache, |
| @GerritServerId String serverId) { |
| this.failOnLoadForTest = new AtomicBoolean(); |
| this.repoManager = repoManager; |
| this.allUsers = allUsers; |
| this.changeNoteJson = changeNoteJson; |
| this.metrics = metrics; |
| this.cache = cache; |
| this.serverId = serverId; |
| } |
| } |
| |
| /** An {@link AutoCloseable} for parsing a single commit into ChangeNotesCommits. */ |
| public static class LoadHandle implements AutoCloseable { |
| private final Repository repo; |
| private final ObjectId id; |
| private ChangeNotesRevWalk rw; |
| |
| private LoadHandle(Repository repo, @Nullable ObjectId id) { |
| this.repo = requireNonNull(repo); |
| |
| if (ObjectId.zeroId().equals(id)) { |
| id = null; |
| } else if (id != null) { |
| id = id.copy(); |
| } |
| this.id = id; |
| } |
| |
| public ChangeNotesRevWalk walk() { |
| if (rw == null) { |
| rw = ChangeNotesCommit.newRevWalk(repo); |
| } |
| return rw; |
| } |
| |
| @Nullable |
| public ObjectId id() { |
| return id; |
| } |
| |
| @Override |
| public void close() { |
| if (rw != null) { |
| rw.close(); |
| } |
| } |
| } |
| |
| protected final Args args; |
| private final Change.Id changeId; |
| |
| private ObjectId revision; |
| private boolean loaded; |
| |
| protected AbstractChangeNotes(Args args, Change.Id changeId, @Nullable ObjectId metaSha1) { |
| this.args = requireNonNull(args); |
| this.changeId = requireNonNull(changeId); |
| this.revision = metaSha1; |
| } |
| |
| protected AbstractChangeNotes(Args args, Change.Id changeId) { |
| this(args, changeId, null); |
| } |
| |
| public Change.Id getChangeId() { |
| return changeId; |
| } |
| |
| /** Returns revision of the metadata that was loaded. */ |
| public ObjectId getRevision() { |
| return revision; |
| } |
| |
| public T load() { |
| try (Repository repo = args.repoManager.openRepository(getProjectName())) { |
| load(repo); |
| return self(); |
| } catch (IOException e) { |
| throw new StorageException(e); |
| } |
| } |
| |
| public T load(Repository repo) { |
| if (loaded) { |
| return self(); |
| } |
| |
| if (args.failOnLoadForTest.get()) { |
| throw new StorageException("Reading from NoteDb is disabled"); |
| } |
| try (Timer0.Context timer = args.metrics.readLatency.start(); |
| // Call openHandle even if reading is disabled, to trigger |
| // auto-rebuilding before this object may get passed to a ChangeUpdate. |
| LoadHandle handle = openHandle(repo, revision)) { |
| revision = handle.id(); |
| onLoad(handle); |
| loaded = true; |
| } catch (ConfigInvalidException | IOException e) { |
| throw new StorageException(e); |
| } |
| return self(); |
| } |
| |
| @Nullable |
| protected ObjectId readRef(Repository repo) throws IOException { |
| Ref ref = repo.getRefDatabase().exactRef(getRefName()); |
| return ref != null ? ref.getObjectId() : null; |
| } |
| |
| /** |
| * Open a handle for reading this entity from a repository. |
| * |
| * <p>Implementations may override this method to provide auto-rebuilding behavior. |
| * |
| * @param repo open repository. |
| * @param id SHA1 of the entity to read from the repository. The SHA1 is not sanity checked and is |
| * assumed to be valid. If null, lookup SHA1 from the /meta ref. |
| * @return handle for reading the entity. |
| * @throws NoSuchChangeException change does not exist. |
| * @throws IOException a repo-level error occurred. |
| */ |
| protected LoadHandle openHandle(Repository repo, @Nullable ObjectId id) |
| throws NoSuchChangeException, IOException { |
| if (id == null) { |
| id = readRef(repo); |
| } |
| |
| return new LoadHandle(repo, id); |
| } |
| |
| public T reload() { |
| loaded = false; |
| return load(); |
| } |
| |
| public ObjectId loadRevision() { |
| if (loaded) { |
| return getRevision(); |
| } |
| try (Repository repo = args.repoManager.openRepository(getProjectName())) { |
| Ref ref = repo.getRefDatabase().exactRef(getRefName()); |
| return ref != null ? ref.getObjectId() : null; |
| } catch (IOException e) { |
| throw new StorageException(e); |
| } |
| } |
| |
| /** Load default values for any instance variables when NoteDb is disabled. */ |
| protected abstract void loadDefaults(); |
| |
| /** |
| * Returns the NameKey for the project where the notes should be stored, which is not necessarily |
| * the same as the change's project. |
| */ |
| public abstract Project.NameKey getProjectName(); |
| |
| /** Returns name of the reference storing this configuration. */ |
| protected abstract String getRefName(); |
| |
| /** Set up the metadata, parsing any state from the loaded revision. */ |
| protected abstract void onLoad(LoadHandle handle) |
| throws NoSuchChangeException, IOException, ConfigInvalidException; |
| |
| @SuppressWarnings("unchecked") |
| protected final T self() { |
| return (T) this; |
| } |
| } |