Support reloading of gerrit.config

This change introduces a mechanism to allow reloading of the
@GerritServerConfig. To execute a reload of gerrit.config, the SSH
command "reload-config" is provided.

Challenges and implementation:
------------------------------
Most callers that inject @GerritServerConfig are @Singletons. These
@Singletons will only parse the GerritServerConfig once and then
build and store more suitable datastructures to work from.

To allow these modules to react on config changes, a caller can now
implement GerritConfigListener, which will enable the caller to receive
notifications of future config updates.

The GerritConfigListener provides:

    void configUpdated(ConfigUpdatedEvent event);

The ConfigUpdatedEvent provides the old config and the new one.
Implementing classes of the listener are expected to do three things:

1. Investigate if any of the updated config values are of interest.
The ConfigUpdatedEvent provides helper methods for this purpose:

    public boolean isSectionUpdated(String section)
    public boolean isValueUpdated(String section, String subsection, String name)
    (+ various overloaded versions of these)

2. React to the configuration changes (if any) and apply them

3. Accept or reject the entries of interest:

    public void accept(Set<ConfigKey> entries)
    public void accept(String section)
    public void reject(Set<ConfigKey> entries)
    (+ various overloaded versions of these)

When a section (or specific config keys) are accepted or rejected, the
ConfigUpdatedEvent will build a diff of what changes were accepted and
present it to the administrator that issued a configuration reload.

For instance, in this case, 4 config rows where added(+), 2 removed(-1)
and 1 modified. All but one change were accepted, hence the user would get
this output:

$ ssh $HOST -p 29418 gerrit reload-config

Accepted configuration changes:
- suggest.maxSuggestedReviewers = 10
- suggest.accounts = 50
* addreviewer.maxAllowed = [100 => 1000]
+ sshd.requestLog = true
+ commentlink.changeid.link = #/q/$1
+ commentlink.changeid.match = (I[0-9a-f]{8,40})

Rejected configuration changes:
+ gc.startTime = Fri 10:30

Documentation:
--------------
We start by documenting the config entries that support config reloads
(which number will be increasing over time).

When we support reloading of the majority of config settings, the
documentation can switch to documenting the config entries that does
_not_ support config reloading instead.

Roadmap:
--------
This change will be followed by a couple of changes that introduces
reloadable config support for: suggest-reviewers, sshd-log and
commentlinks.

Other stuff TODO includes:
* REST end point
* Rejection messages (to allow for an explanation on why the value was
not accepted)
* Test cases
* An option to allow displaying the current effective configuration
* A lot of implementations of the GerritConfigListener.

Change-Id: I4bd6f389731af303ef9ba5d1d73f173d869c62e4
This commit is contained in:
Gustaf Lundh
2018-04-16 10:51:47 +02:00
parent 7ad00f81af
commit e0f52ff170
12 changed files with 493 additions and 6 deletions

View File

@@ -0,0 +1,214 @@
// Copyright (C) 2018 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.config;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.lib.Config;
/**
* This event is produced by {@link GerritServerConfigReloader} and forwarded to callers
* implementing {@link GerritConfigListener}.
*
* <p>The event intends to:
*
* <p>1. Help the callers figure out if any action should be taken, depending on which entries are
* updated in gerrit.config.
*
* <p>2. Provide the callers with a mechanism to accept/reject the entries of interest: @see
* accept(Set<ConfigKey> entries), @see accept(String section), @see reject(Set<ConfigKey> entries)
* (+ various overloaded versions of these)
*/
public class ConfigUpdatedEvent {
private final Config oldConfig;
private final Config newConfig;
public ConfigUpdatedEvent(Config oldConfig, Config newConfig) {
this.oldConfig = oldConfig;
this.newConfig = newConfig;
}
public Config getOldConfig() {
return this.oldConfig;
}
public Config getNewConfig() {
return this.newConfig;
}
public Update accept(ConfigKey entry) {
return accept(Collections.singleton(entry));
}
public Update accept(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.APPLIED);
}
public Update accept(String section) {
Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
entries.addAll(getEntriesFromSection(newConfig, section));
return createUpdate(entries, UpdateResult.APPLIED);
}
public Update reject(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.REJECTED);
}
private static Set<ConfigKey> getEntriesFromSection(Config config, String section) {
Set<ConfigKey> res = new LinkedHashSet<>();
for (String name : config.getNames(section, true)) {
res.add(ConfigKey.create(section, name));
}
for (String sub : config.getSubsections(section)) {
for (String name : config.getNames(section, sub, true)) {
res.add(ConfigKey.create(section, sub, name));
}
}
return res;
}
private Update createUpdate(Set<ConfigKey> entries, UpdateResult updateResult) {
Update update = new Update(updateResult);
entries
.stream()
.filter(this::isValueUpdated)
.forEach(
key -> {
update.addConfigUpdate(
new ConfigUpdateEntry(
key,
oldConfig.getString(key.section(), key.subsection(), key.name()),
newConfig.getString(key.section(), key.subsection(), key.name())));
});
return update;
}
public boolean isSectionUpdated(String section) {
Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
entries.addAll(getEntriesFromSection(newConfig, section));
return isEntriesUpdated(entries);
}
public boolean isValueUpdated(String section, String subsection, String name) {
return !Objects.equals(
oldConfig.getString(section, subsection, name),
newConfig.getString(section, subsection, name));
}
public boolean isValueUpdated(ConfigKey key) {
return isValueUpdated(key.section(), key.subsection(), key.name());
}
public boolean isValueUpdated(String section, String name) {
return isValueUpdated(section, null, name);
}
public boolean isEntriesUpdated(Set<ConfigKey> entries) {
for (ConfigKey entry : entries) {
if (isValueUpdated(entry.section(), entry.subsection(), entry.name())) {
return true;
}
}
return false;
}
public enum UpdateResult {
APPLIED,
REJECTED;
@Override
public String toString() {
return StringUtils.capitalize(name().toLowerCase());
}
}
/**
* One Accepted/Rejected Update have one or more config updates (ConfigUpdateEntry) tied to it.
*/
public static class Update {
private UpdateResult result;
private final Set<ConfigUpdateEntry> configUpdates;
public Update(UpdateResult result) {
this.configUpdates = new LinkedHashSet<>();
this.result = result;
}
public UpdateResult getResult() {
return result;
}
public List<ConfigUpdateEntry> getConfigUpdates() {
return ImmutableList.copyOf(configUpdates);
}
public void addConfigUpdate(ConfigUpdateEntry entry) {
this.configUpdates.add(entry);
}
}
public enum ConfigEntryType {
ADDED,
REMOVED,
MODIFIED,
UNMODIFIED
}
public static class ConfigUpdateEntry {
public final ConfigKey key;
public final String oldVal;
public final String newVal;
public ConfigUpdateEntry(ConfigKey key, String oldVal, String newVal) {
this.key = key;
this.oldVal = oldVal;
this.newVal = newVal;
}
/** Note: The toString() is used to format the output from @see ReloadConfig. */
@Override
public String toString() {
switch (getUpdateType()) {
case ADDED:
return String.format("+ %s = %s", key, newVal);
case MODIFIED:
return String.format("* %s = [%s => %s]", key, oldVal, newVal);
case REMOVED:
return String.format("- %s = %s", key, oldVal);
case UNMODIFIED:
return String.format(" %s = %s", key, newVal);
default:
throw new IllegalStateException("Unexpected UpdateType: " + getUpdateType().name());
}
}
public ConfigEntryType getUpdateType() {
if (oldVal == null && newVal != null) {
return ConfigEntryType.ADDED;
}
if (oldVal != null && newVal == null) {
return ConfigEntryType.REMOVED;
}
if (Objects.equals(oldVal, newVal)) {
return ConfigEntryType.UNMODIFIED;
}
return ConfigEntryType.MODIFIED;
}
}
}