Cache additional *.config files in CachedProjectConfig
ProjectLevelConfigs represent additional configs stored in refs/meta/config. They used to be loaded on demand and cached. This wasn't thread safe and mutated a cached object. This commit removes this behavior and loads all additional configs when the project.config is loaded. Configs are stored as strings to keep them immutable. Change-Id: I2379e28b484550b2372b7f530ffda7639ed1a9d6
This commit is contained in:
		@@ -14,6 +14,8 @@
 | 
			
		||||
 | 
			
		||||
package com.google.gerrit.entities;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.base.Preconditions.checkState;
 | 
			
		||||
 | 
			
		||||
import com.google.auto.value.AutoValue;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.ImmutableMap;
 | 
			
		||||
@@ -23,6 +25,8 @@ import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import org.eclipse.jgit.annotations.Nullable;
 | 
			
		||||
import org.eclipse.jgit.errors.ConfigInvalidException;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
import org.eclipse.jgit.lib.ObjectId;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -122,6 +126,34 @@ public abstract class CachedProjectConfig {
 | 
			
		||||
 | 
			
		||||
  public abstract ImmutableMap<String, String> getPluginConfigs();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the {@link Config} that got parsed from the specified {@code fileName} on {@code
 | 
			
		||||
   * refs/meta/config}. The returned instance is a defensive copy of the cached value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param fileName the name of the file. Must end in {@code .config}.
 | 
			
		||||
   * @return an {@link Optional} of the {@link Config}. {@link Optional#empty()} if the file was not
 | 
			
		||||
   *     found or could not be parsed. {@link com.google.gerrit.server.project.ProjectConfig} will
 | 
			
		||||
   *     surface validation errors in case of a parsing issue.
 | 
			
		||||
   */
 | 
			
		||||
  public Optional<Config> getProjectLevelConfig(String fileName) {
 | 
			
		||||
    checkState(fileName.endsWith(".config"), "file name must end in .config");
 | 
			
		||||
    if (getProjectLevelConfigs().containsKey(fileName)) {
 | 
			
		||||
      Config config = new Config();
 | 
			
		||||
      try {
 | 
			
		||||
        config.fromText(getProjectLevelConfigs().get(fileName));
 | 
			
		||||
      } catch (ConfigInvalidException e) {
 | 
			
		||||
        // This is OK to propagate as IllegalStateException because it's a programmer error.
 | 
			
		||||
        // The config was converted to a String using Config#toText. So #fromText must not
 | 
			
		||||
        // throw a ConfigInvalidException
 | 
			
		||||
        throw new IllegalStateException("invalid config for " + fileName, e);
 | 
			
		||||
      }
 | 
			
		||||
      return Optional.of(config);
 | 
			
		||||
    }
 | 
			
		||||
    return Optional.empty();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abstract ImmutableMap<String, String> getProjectLevelConfigs();
 | 
			
		||||
 | 
			
		||||
  public static Builder builder() {
 | 
			
		||||
    return new AutoValue_CachedProjectConfig.Builder();
 | 
			
		||||
  }
 | 
			
		||||
@@ -201,6 +233,13 @@ public abstract class CachedProjectConfig {
 | 
			
		||||
      return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract ImmutableMap.Builder<String, String> projectLevelConfigsBuilder();
 | 
			
		||||
 | 
			
		||||
    public Builder addProjectLevelConfig(String configFileName, String config) {
 | 
			
		||||
      projectLevelConfigsBuilder().put(configFileName, config);
 | 
			
		||||
      return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract CachedProjectConfig build();
 | 
			
		||||
 | 
			
		||||
    protected abstract ImmutableMap.Builder<AccountGroup.UUID, GroupReference> groupsBuilder();
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ import com.google.common.base.Strings;
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.Maps;
 | 
			
		||||
import com.google.common.collect.Sets;
 | 
			
		||||
import com.google.common.flogger.FluentLogger;
 | 
			
		||||
import com.google.common.primitives.Shorts;
 | 
			
		||||
import com.google.gerrit.common.Nullable;
 | 
			
		||||
import com.google.gerrit.common.UsedAt;
 | 
			
		||||
@@ -99,6 +100,8 @@ import org.eclipse.jgit.storage.file.FileBasedConfig;
 | 
			
		||||
import org.eclipse.jgit.util.FS;
 | 
			
		||||
 | 
			
		||||
public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
 | 
			
		||||
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 | 
			
		||||
 | 
			
		||||
  public static final String COMMENTLINK = "commentlink";
 | 
			
		||||
  public static final String LABEL = "label";
 | 
			
		||||
  public static final String KEY_FUNCTION = "function";
 | 
			
		||||
@@ -250,6 +253,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
 | 
			
		||||
  private ObjectId rulesId;
 | 
			
		||||
  private long maxObjectSizeLimit;
 | 
			
		||||
  private Map<String, Config> pluginConfigs;
 | 
			
		||||
  private Map<String, Config> projectLevelConfigs;
 | 
			
		||||
  private boolean checkReceivedObjects;
 | 
			
		||||
  private Set<String> sectionsWithUnknownPermissions;
 | 
			
		||||
  private boolean hasLegacyPermissions;
 | 
			
		||||
@@ -278,6 +282,9 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
 | 
			
		||||
    pluginConfigs
 | 
			
		||||
        .entrySet()
 | 
			
		||||
        .forEach(c -> builder.addPluginConfig(c.getKey(), c.getValue().toText()));
 | 
			
		||||
    projectLevelConfigs
 | 
			
		||||
        .entrySet()
 | 
			
		||||
        .forEach(c -> builder.addProjectLevelConfig(c.getKey(), c.getValue().toText()));
 | 
			
		||||
    return builder.build();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -655,6 +662,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
 | 
			
		||||
    loadSubscribeSections(rc);
 | 
			
		||||
    mimeTypes = ConfiguredMimeTypes.create(projectName.get(), rc);
 | 
			
		||||
    loadPluginSections(rc);
 | 
			
		||||
    loadProjectLevelConfigs();
 | 
			
		||||
    loadReceiveSection(rc);
 | 
			
		||||
    loadExtensionPanelSections(rc);
 | 
			
		||||
  }
 | 
			
		||||
@@ -1176,6 +1184,25 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
 | 
			
		||||
    return PluginConfig.create(pluginName, pluginConfig, getCacheable());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void loadProjectLevelConfigs() throws IOException {
 | 
			
		||||
    projectLevelConfigs = new HashMap<>();
 | 
			
		||||
    if (revision == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    for (PathInfo pathInfo : getPathInfos(true)) {
 | 
			
		||||
      if (pathInfo.path.endsWith(".config") && !PROJECT_CONFIG.equals(pathInfo.path)) {
 | 
			
		||||
        String cfg = readUTF8(pathInfo.path);
 | 
			
		||||
        Config parsedConfig = new Config();
 | 
			
		||||
        try {
 | 
			
		||||
          parsedConfig.fromText(cfg);
 | 
			
		||||
          projectLevelConfigs.put(pathInfo.path, parsedConfig);
 | 
			
		||||
        } catch (ConfigInvalidException e) {
 | 
			
		||||
          logger.atWarning().withCause(e).log("Unable to parse config");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private void readGroupList() throws IOException {
 | 
			
		||||
    groupList = GroupList.parse(projectName, readUTF8(GroupList.FILE_NAME), this);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,29 +24,61 @@ import java.io.IOException;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
import org.eclipse.jgit.annotations.Nullable;
 | 
			
		||||
import org.eclipse.jgit.errors.ConfigInvalidException;
 | 
			
		||||
import org.eclipse.jgit.lib.CommitBuilder;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
 | 
			
		||||
/** Configuration file in the projects refs/meta/config branch. */
 | 
			
		||||
public class ProjectLevelConfig extends VersionedMetaData {
 | 
			
		||||
public class ProjectLevelConfig {
 | 
			
		||||
  /**
 | 
			
		||||
   * This class is a low-level API that allows callers to read the config directly from a repository
 | 
			
		||||
   * and make updates to it.
 | 
			
		||||
   */
 | 
			
		||||
  public static class Bare extends VersionedMetaData {
 | 
			
		||||
    private final String fileName;
 | 
			
		||||
    @Nullable private Config cfg;
 | 
			
		||||
 | 
			
		||||
    public Bare(String fileName) {
 | 
			
		||||
      this.fileName = fileName;
 | 
			
		||||
      this.cfg = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Config getConfig() {
 | 
			
		||||
      if (cfg == null) {
 | 
			
		||||
        cfg = new Config();
 | 
			
		||||
      }
 | 
			
		||||
      return cfg;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getRefName() {
 | 
			
		||||
      return RefNames.REFS_CONFIG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onLoad() throws IOException, ConfigInvalidException {
 | 
			
		||||
      cfg = readConfig(fileName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean onSave(CommitBuilder commit) throws IOException {
 | 
			
		||||
      if (commit.getMessage() == null || "".equals(commit.getMessage())) {
 | 
			
		||||
        commit.setMessage("Updated configuration\n");
 | 
			
		||||
      }
 | 
			
		||||
      saveConfig(fileName, cfg);
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private final String fileName;
 | 
			
		||||
  private final ProjectState project;
 | 
			
		||||
  private Config cfg;
 | 
			
		||||
 | 
			
		||||
  public ProjectLevelConfig(String fileName, ProjectState project) {
 | 
			
		||||
  public ProjectLevelConfig(String fileName, ProjectState project, Config cfg) {
 | 
			
		||||
    this.fileName = fileName;
 | 
			
		||||
    this.project = project;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  protected String getRefName() {
 | 
			
		||||
    return RefNames.REFS_CONFIG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  protected void onLoad() throws IOException, ConfigInvalidException {
 | 
			
		||||
    cfg = readConfig(fileName);
 | 
			
		||||
    this.cfg = cfg;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public Config get() {
 | 
			
		||||
@@ -127,13 +159,4 @@ public class ProjectLevelConfig extends VersionedMetaData {
 | 
			
		||||
    }
 | 
			
		||||
    return cfgWithInheritance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
 | 
			
		||||
    if (commit.getMessage() == null || "".equals(commit.getMessage())) {
 | 
			
		||||
      commit.setMessage("Updated configuration\n");
 | 
			
		||||
    }
 | 
			
		||||
    saveConfig(fileName, cfg);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,16 +43,13 @@ import com.google.gerrit.server.account.CapabilityCollection;
 | 
			
		||||
import com.google.gerrit.server.config.AllProjectsName;
 | 
			
		||||
import com.google.gerrit.server.config.AllUsersName;
 | 
			
		||||
import com.google.gerrit.server.config.PluginConfig;
 | 
			
		||||
import com.google.gerrit.server.git.GitRepositoryManager;
 | 
			
		||||
import com.google.gerrit.server.git.TransferConfig;
 | 
			
		||||
import com.google.gerrit.server.notedb.ChangeNotes;
 | 
			
		||||
import com.google.inject.Inject;
 | 
			
		||||
import com.google.inject.assistedinject.Assisted;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.LinkedHashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -61,7 +58,6 @@ import java.util.Optional;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import org.eclipse.jgit.errors.ConfigInvalidException;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
import org.eclipse.jgit.lib.Repository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Cached information on a project. Must not contain any data derived from parents other than it's
 | 
			
		||||
@@ -78,11 +74,9 @@ public class ProjectState {
 | 
			
		||||
  private final boolean isAllUsers;
 | 
			
		||||
  private final AllProjectsName allProjectsName;
 | 
			
		||||
  private final ProjectCache projectCache;
 | 
			
		||||
  private final GitRepositoryManager gitMgr;
 | 
			
		||||
  private final List<CommentLinkInfo> commentLinks;
 | 
			
		||||
 | 
			
		||||
  private final CachedProjectConfig cachedConfig;
 | 
			
		||||
  private final Map<String, ProjectLevelConfig> configs;
 | 
			
		||||
  private final Set<AccountGroup.UUID> localOwners;
 | 
			
		||||
  private final long globalMaxObjectSizeLimit;
 | 
			
		||||
  private final boolean inheritProjectMaxObjectSizeLimit;
 | 
			
		||||
@@ -98,7 +92,6 @@ public class ProjectState {
 | 
			
		||||
      ProjectCache projectCache,
 | 
			
		||||
      AllProjectsName allProjectsName,
 | 
			
		||||
      AllUsersName allUsersName,
 | 
			
		||||
      GitRepositoryManager gitMgr,
 | 
			
		||||
      List<CommentLinkInfo> commentLinks,
 | 
			
		||||
      CapabilityCollection.Factory limitsFactory,
 | 
			
		||||
      TransferConfig transferConfig,
 | 
			
		||||
@@ -107,10 +100,8 @@ public class ProjectState {
 | 
			
		||||
    this.isAllProjects = cachedProjectConfig.getProject().getNameKey().equals(allProjectsName);
 | 
			
		||||
    this.isAllUsers = cachedProjectConfig.getProject().getNameKey().equals(allUsersName);
 | 
			
		||||
    this.allProjectsName = allProjectsName;
 | 
			
		||||
    this.gitMgr = gitMgr;
 | 
			
		||||
    this.commentLinks = commentLinks;
 | 
			
		||||
    this.cachedConfig = cachedProjectConfig;
 | 
			
		||||
    this.configs = new HashMap<>();
 | 
			
		||||
    this.capabilities =
 | 
			
		||||
        isAllProjects
 | 
			
		||||
            ? limitsFactory.create(
 | 
			
		||||
@@ -183,19 +174,8 @@ public class ProjectState {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public ProjectLevelConfig getConfig(String fileName) {
 | 
			
		||||
    if (configs.containsKey(fileName)) {
 | 
			
		||||
      return configs.get(fileName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
 | 
			
		||||
    try (Repository git = gitMgr.openRepository(getNameKey())) {
 | 
			
		||||
      cfg.load(getNameKey(), git, cachedConfig.getRevision().get());
 | 
			
		||||
    } catch (IOException | ConfigInvalidException e) {
 | 
			
		||||
      logger.atWarning().withCause(e).log("Failed to load %s for %s", fileName, getName());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    configs.put(fileName, cfg);
 | 
			
		||||
    return cfg;
 | 
			
		||||
    Optional<Config> rawConfig = cachedConfig.getProjectLevelConfig(fileName);
 | 
			
		||||
    return new ProjectLevelConfig(fileName, this, rawConfig.orElse(new Config()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public long getMaxObjectSizeLimit() {
 | 
			
		||||
 
 | 
			
		||||
@@ -173,4 +173,16 @@ public class ProjectLevelConfigIT extends AbstractDaemonTest {
 | 
			
		||||
 | 
			
		||||
    assertThat(state.getConfig(configName).get().toText()).isEqualTo(cfg.toText());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void brokenConfigDoesNotBlockPush() throws Exception {
 | 
			
		||||
    String configName = "test.config";
 | 
			
		||||
    PushOneCommit push =
 | 
			
		||||
        pushFactory.create(
 | 
			
		||||
            admin.newIdent(), testRepo, "Create Project Level Config", configName, "\\\\///");
 | 
			
		||||
    push.to(RefNames.REFS_CONFIG).assertOkStatus();
 | 
			
		||||
 | 
			
		||||
    ProjectState state = projectCache.get(project).get();
 | 
			
		||||
    assertThat(state.getConfig(configName).get().toText()).isEmpty();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user