| // Copyright (C) 2021 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.googlesource.gerrit.plugins.healthcheck.check; |
| |
| import static java.util.stream.Collectors.groupingBy; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.inject.Inject; |
| import com.googlesource.gerrit.plugins.healthcheck.HealthCheckConfig; |
| import com.googlesource.gerrit.plugins.healthcheck.check.BlockedThreadsCheck.Collector; |
| import com.googlesource.gerrit.plugins.healthcheck.check.BlockedThreadsCheck.CollectorProvider; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @VisibleForTesting |
| public class BlockedThreadsConfigurator { |
| private static final Logger log = LoggerFactory.getLogger(BlockedThreadsConfigurator.class); |
| private static final Pattern THRESHOLD_PATTERN = Pattern.compile("^(\\d\\d?)$"); |
| |
| static final int DEFAULT_BLOCKED_THREADS_THRESHOLD = 50; |
| |
| private final List<CollectorProvider<?>> providers; |
| |
| @Inject |
| BlockedThreadsConfigurator( |
| BlockedThreadsSubCheck.Factory subchecks, HealthCheckConfig healthCheckConfig) { |
| this.providers = getProviders(subchecks, healthCheckConfig); |
| } |
| |
| List<Collector> createCollectors() { |
| return providers.stream().map(CollectorProvider::get).collect(toList()); |
| } |
| |
| private static List<CollectorProvider<?>> getProviders( |
| BlockedThreadsSubCheck.Factory subchecksFactory, HealthCheckConfig healthCheckConfig) { |
| return getConfig(healthCheckConfig.getListOfBlockedThreadsThresholds()).stream() |
| .map(spec -> collectorProvider(subchecksFactory, spec)) |
| .collect(toList()); |
| } |
| |
| private static CollectorProvider<?> collectorProvider( |
| BlockedThreadsSubCheck.Factory subchecksFactory, Threshold spec) { |
| return spec.prefix.isPresent() |
| ? subchecksFactory.create(spec.prefix.get(), spec.value) |
| : () -> new BlockedThreadsCheck.Collector(spec.value); |
| } |
| |
| @VisibleForTesting |
| static Collection<Threshold> getConfig(String[] thresholds) { |
| // Threshold can be defined as a sole value e.g |
| // threshold = 80 |
| // and would become a default one for all blocked threads check or as a set of specific thread |
| // groups checks defined like |
| // threshold = foo=30 |
| // threshold = bar=40 |
| // ... |
| // they are mutually exclusive which means that one either checks all threads or groups |
| Map<Boolean, List<Threshold>> specsClassified = |
| Arrays.stream(thresholds) |
| .filter(spec -> !spec.isEmpty()) |
| .map(BlockedThreadsConfigurator::getSpec) |
| .filter(Optional::isPresent) |
| .map(Optional::get) |
| .collect(groupingBy(Threshold::hasPrefix)); |
| |
| // check configuration consistency |
| if (specsClassified.size() > 1) { |
| Collection<Threshold> specs = deduplicatePrefixes(specsClassified.get(true)); |
| log.warn( |
| "Global and specific thresholds were configured for blocked threads check. Specific" |
| + " configuration is used {}.", |
| specs); |
| return specs; |
| } |
| |
| if (specsClassified.size() == 1) { |
| Map.Entry<Boolean, List<Threshold>> entry = specsClassified.entrySet().iterator().next(); |
| return Boolean.TRUE == entry.getKey() |
| ? deduplicatePrefixes(entry.getValue()) |
| : deduplicateGlobal(entry.getValue()); |
| } |
| |
| log.info( |
| "Default blocked threads check is configured with {}% threshold", |
| DEFAULT_BLOCKED_THREADS_THRESHOLD); |
| return ImmutableSet.of(new Threshold(DEFAULT_BLOCKED_THREADS_THRESHOLD)); |
| } |
| |
| private static Collection<Threshold> deduplicateGlobal(List<Threshold> input) { |
| if (input.size() > 1) { |
| Threshold spec = input.get(input.size() - 1); |
| log.warn("Multiple threshold values were configured. Using {}", spec); |
| return ImmutableSet.of(spec); |
| } |
| return input; |
| } |
| |
| private static Collection<Threshold> deduplicatePrefixes(Collection<Threshold> input) { |
| Map<String, Threshold> deduplicated = new HashMap<>(); |
| input.forEach(t -> deduplicated.put(t.prefix.get(), t)); |
| if (deduplicated.size() != input.size()) { |
| log.warn( |
| "The same prefixes were configured multiple times. The following configuration is used" |
| + " {}", |
| deduplicated.values()); |
| } |
| return deduplicated.values(); |
| } |
| |
| private static Optional<Threshold> getSpec(String spec) { |
| int equals = spec.lastIndexOf('='); |
| if (equals != -1) { |
| Optional<Integer> maybeThreshold = isThresholdDefined(spec.substring(equals + 1)); |
| if (maybeThreshold.isPresent()) { |
| return Optional.of(new Threshold(spec.substring(0, equals).trim(), maybeThreshold.get())); |
| } |
| } else { |
| Optional<Integer> maybeThreshold = isThresholdDefined(spec); |
| if (maybeThreshold.isPresent()) { |
| return Optional.of(new Threshold(maybeThreshold.get())); |
| } |
| } |
| |
| log.warn("Invalid configuration of blocked threads threshold [{}]", spec); |
| return Optional.empty(); |
| } |
| |
| private static Optional<Integer> isThresholdDefined(String input) { |
| Matcher value = THRESHOLD_PATTERN.matcher(input.trim()); |
| if (value.matches()) { |
| return Optional.of(Integer.valueOf(value.group(1))); |
| } |
| return Optional.empty(); |
| } |
| |
| @VisibleForTesting |
| static class Threshold { |
| final Optional<String> prefix; |
| final Integer value; |
| |
| Threshold(int value) { |
| this(null, value); |
| } |
| |
| Threshold(String prefix, int value) { |
| this.prefix = Optional.ofNullable(prefix); |
| this.value = value; |
| } |
| |
| boolean hasPrefix() { |
| return prefix.isPresent(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(prefix, value); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null || getClass() != obj.getClass()) { |
| return false; |
| } |
| Threshold other = (Threshold) obj; |
| return Objects.equals(prefix, other.prefix) && Objects.equals(value, other.value); |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("Threshold [prefix=") |
| .append(prefix) |
| .append(", value=") |
| .append(value) |
| .append("]") |
| .toString(); |
| } |
| } |
| } |