LCOV - code coverage report
Current view: top level - server - DeadlineChecker.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 107 109 98.2 %
Date: 2022-11-19 15:00:39 Functions: 19 19 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2021 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server;
      16             : 
      17             : import static java.util.Comparator.comparing;
      18             : import static java.util.Objects.requireNonNull;
      19             : import static java.util.concurrent.TimeUnit.MILLISECONDS;
      20             : import static java.util.concurrent.TimeUnit.MINUTES;
      21             : import static java.util.concurrent.TimeUnit.NANOSECONDS;
      22             : import static java.util.stream.Collectors.toMap;
      23             : 
      24             : import com.google.auto.value.AutoValue;
      25             : import com.google.common.base.Strings;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.flogger.FluentLogger;
      28             : import com.google.common.primitives.Longs;
      29             : import com.google.gerrit.common.Nullable;
      30             : import com.google.gerrit.server.cancellation.RequestStateProvider;
      31             : import com.google.gerrit.server.config.ConfigUtil;
      32             : import com.google.gerrit.server.config.GerritServerConfig;
      33             : import com.google.gerrit.server.util.time.TimeUtil;
      34             : import com.google.inject.assistedinject.Assisted;
      35             : import com.google.inject.assistedinject.AssistedInject;
      36             : import java.util.HashSet;
      37             : import java.util.Map;
      38             : import java.util.Optional;
      39             : import java.util.Set;
      40             : import java.util.concurrent.TimeUnit;
      41             : import java.util.function.Function;
      42             : import org.eclipse.jgit.lib.Config;
      43             : 
      44             : /**
      45             :  * {@link RequestStateProvider} that checks whether a client provided deadline is exceeded.
      46             :  *
      47             :  * <p>Should be registered at most once per request.
      48             :  */
      49             : public class DeadlineChecker implements RequestStateProvider {
      50         104 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      51             : 
      52         104 :   private static String SECTION_DEADLINE = "deadline";
      53             : 
      54             :   /**
      55             :    * Creates a formatter that formats a timeout as {@code <TIMEOUT_NAME>=<TIMEOUT><TIME_UNIT>}.
      56             :    *
      57             :    * <p>If the timeout is 1 minute or greater, minutes is used as a time unit. Otherwise
      58             :    * milliseconds is just as a time unit.
      59             :    *
      60             :    * @param timeoutName the name of the timeout
      61             :    */
      62             :   public static Function<Long, String> getTimeoutFormatter(String timeoutName) {
      63           2 :     requireNonNull(timeoutName, "timeoutName");
      64           2 :     return timeout -> {
      65           2 :       String formattedTimeout = MILLISECONDS.convert(timeout, NANOSECONDS) + "ms";
      66           2 :       long timeoutInMinutes = MINUTES.convert(timeout, NANOSECONDS);
      67           2 :       if (timeoutInMinutes > 0) {
      68           0 :         formattedTimeout = timeoutInMinutes + "m";
      69             :       }
      70           2 :       return String.format("%s=%s", timeoutName, formattedTimeout);
      71             :     };
      72             :   }
      73             : 
      74             :   public interface Factory {
      75             :     DeadlineChecker create(RequestInfo requestInfo, @Nullable String clientProvidedTimeoutValue)
      76             :         throws InvalidDeadlineException;
      77             : 
      78             :     DeadlineChecker create(
      79             :         long start, RequestInfo requestInfo, @Nullable String clientProvidedTimeoutValue)
      80             :         throws InvalidDeadlineException;
      81             :   }
      82             : 
      83             :   private final CancellationMetrics cancellationsMetrics;
      84             : 
      85             :   /** The start time of the request in nanoseconds. */
      86             :   private final long start;
      87             : 
      88             :   private final RequestInfo requestInfo;
      89             :   private final RequestStateProvider.Reason cancellationReason;
      90             :   private final String timeoutName;
      91             : 
      92             :   /**
      93             :    * Timeout in nanoseconds after which the request should be aborted.
      94             :    *
      95             :    * <p>{@code 0} means that no timeout should be applied.
      96             :    */
      97             :   private final long timeout;
      98             : 
      99             :   /**
     100             :    * The deadline in nanoseconds after which a request should be aborted.
     101             :    *
     102             :    * <p>deadline = start + timeout
     103             :    *
     104             :    * <p>{@link Optional#empty()} if no timeout was set.
     105             :    */
     106             :   private final Optional<Long> deadline;
     107             : 
     108             :   /**
     109             :    * Matching server side deadlines that have been configured as as advisory.
     110             :    *
     111             :    * <p>If any of these deadlines is exceeded the request is not be aborted. Instead the {@code
     112             :    * cancellation/advisory_deadline_count} metric is incremented and a log is written.
     113             :    */
     114             :   private final Map<String, ServerDeadline> advisoryDeadlines;
     115             : 
     116             :   /**
     117             :    * Creates a {@link DeadlineChecker}.
     118             :    *
     119             :    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
     120             :    *
     121             :    * @param requestInfo the request that was received from a user
     122             :    * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
     123             :    *     numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
     124             :    *     be {@code null}
     125             :    * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
     126             :    *     e.g. because it uses a bad time unit
     127             :    */
     128             :   @AssistedInject
     129             :   DeadlineChecker(
     130             :       @GerritServerConfig Config serverConfig,
     131             :       CancellationMetrics cancellationsMetrics,
     132             :       @Assisted RequestInfo requestInfo,
     133             :       @Assisted @Nullable String clientProvidedTimeoutValue)
     134             :       throws InvalidDeadlineException {
     135          32 :     this(
     136             :         serverConfig,
     137             :         cancellationsMetrics,
     138          32 :         TimeUtil.nowNanos(),
     139             :         requestInfo,
     140             :         clientProvidedTimeoutValue);
     141          32 :   }
     142             : 
     143             :   /**
     144             :    * Creates a {@link DeadlineChecker}.
     145             :    *
     146             :    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
     147             :    *
     148             :    * @param start the start time of the request in nanoseconds
     149             :    * @param requestInfo the request that was received from a user
     150             :    * @param clientProvidedTimeoutValue the timeout value that the client provided, must represent a
     151             :    *     numerical time unit (e.g. "5m"), if no time unit is specified milliseconds are assumed, may
     152             :    *     be {@code null}
     153             :    * @throws InvalidDeadlineException thrown if the client provided deadline value cannot be parsed,
     154             :    *     e.g. because it uses a bad time unit
     155             :    */
     156             :   @AssistedInject
     157             :   DeadlineChecker(
     158             :       @GerritServerConfig Config serverConfig,
     159             :       CancellationMetrics cancellationsMetrics,
     160             :       @Assisted long start,
     161             :       @Assisted RequestInfo requestInfo,
     162             :       @Assisted @Nullable String clientProvidedTimeoutValue)
     163         104 :       throws InvalidDeadlineException {
     164         104 :     this.cancellationsMetrics = cancellationsMetrics;
     165         104 :     this.start = start;
     166         104 :     this.requestInfo = requestInfo;
     167             : 
     168         104 :     ImmutableList<RequestConfig> deadlineConfigs =
     169         104 :         RequestConfig.parseConfigs(serverConfig, SECTION_DEADLINE);
     170         104 :     advisoryDeadlines = getAdvisoryDeadlines(deadlineConfigs, requestInfo);
     171         104 :     Optional<ServerDeadline> serverSideDeadline =
     172         104 :         getServerSideDeadline(deadlineConfigs, requestInfo);
     173         104 :     Optional<Long> clientedProvidedTimeout = parseTimeout(clientProvidedTimeoutValue);
     174         104 :     logDeadlines(serverSideDeadline, clientedProvidedTimeout);
     175             : 
     176         104 :     this.cancellationReason =
     177         104 :         clientedProvidedTimeout.isPresent()
     178           2 :             ? RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED
     179         104 :             : RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED;
     180         104 :     this.timeoutName =
     181             :         clientedProvidedTimeout
     182         104 :             .map(clientTimeout -> "client.timeout")
     183         104 :             .orElse(
     184             :                 serverSideDeadline
     185         104 :                     .map(serverDeadline -> serverDeadline.id() + ".timeout")
     186         104 :                     .orElse("timeout"));
     187         104 :     this.timeout =
     188         104 :         clientedProvidedTimeout.orElse(serverSideDeadline.map(ServerDeadline::timeout).orElse(0L));
     189         104 :     this.deadline = timeout > 0 ? Optional.of(start + timeout) : Optional.empty();
     190         104 :   }
     191             : 
     192             :   private void logDeadlines(
     193             :       Optional<ServerDeadline> serverSideDeadline, Optional<Long> clientedProvidedTimeout) {
     194         104 :     if (serverSideDeadline.isPresent()) {
     195           2 :       if (clientedProvidedTimeout.isPresent()) {
     196           2 :         logger.atFine().log(
     197             :             "client provided deadline (timeout=%sms) overrides server deadline %s (timeout=%sms)",
     198           2 :             TimeUnit.MILLISECONDS.convert(clientedProvidedTimeout.get(), TimeUnit.NANOSECONDS),
     199           2 :             serverSideDeadline.get().id(),
     200           2 :             TimeUnit.MILLISECONDS.convert(
     201           2 :                 serverSideDeadline.get().timeout(), TimeUnit.NANOSECONDS));
     202             :       } else {
     203           2 :         logger.atFine().log(
     204             :             "applying server deadline %s (timeout = %sms)",
     205           2 :             serverSideDeadline.get().id(),
     206           2 :             TimeUnit.MILLISECONDS.convert(
     207           2 :                 serverSideDeadline.get().timeout(), TimeUnit.NANOSECONDS));
     208             :       }
     209         104 :     } else if (clientedProvidedTimeout.isPresent()) {
     210           2 :       logger.atFine().log(
     211             :           "applying client provided deadline (timeout = %sms)",
     212           2 :           TimeUnit.MILLISECONDS.convert(clientedProvidedTimeout.get(), TimeUnit.NANOSECONDS));
     213             :     }
     214         104 :   }
     215             : 
     216             :   private Optional<ServerDeadline> getServerSideDeadline(
     217             :       ImmutableList<RequestConfig> deadlineConfigs, RequestInfo requestInfo) {
     218         104 :     return deadlineConfigs.stream()
     219         104 :         .filter(deadlineConfig -> deadlineConfig.matches(requestInfo))
     220         104 :         .map(ServerDeadline::readFrom)
     221         104 :         .filter(ServerDeadline::hasTimeout)
     222         104 :         .filter(deadline -> !deadline.isAdvisory())
     223             :         // let the stricter deadline (the lower deadline) take precedence
     224         104 :         .sorted(comparing(ServerDeadline::timeout))
     225         104 :         .findFirst();
     226             :   }
     227             : 
     228             :   private Map<String, ServerDeadline> getAdvisoryDeadlines(
     229             :       ImmutableList<RequestConfig> deadlineConfigs, RequestInfo requestInfo) {
     230         104 :     return deadlineConfigs.stream()
     231         104 :         .filter(deadlineConfig -> deadlineConfig.matches(requestInfo))
     232         104 :         .map(ServerDeadline::readFrom)
     233         104 :         .filter(ServerDeadline::hasTimeout)
     234         104 :         .filter(ServerDeadline::isAdvisory)
     235         104 :         .collect(toMap(ServerDeadline::id, Function.identity()));
     236             :   }
     237             : 
     238             :   @Override
     239             :   public void checkIfCancelled(OnCancelled onCancelled) {
     240         104 :     long now = TimeUtil.nowNanos();
     241             : 
     242         104 :     Set<String> exceededAdvisoryDeadlines = new HashSet<>();
     243         104 :     advisoryDeadlines
     244         104 :         .values()
     245         104 :         .forEach(
     246             :             advisoryDeadline -> {
     247           1 :               if (now > start + advisoryDeadline.timeout()) {
     248           1 :                 exceededAdvisoryDeadlines.add(advisoryDeadline.id());
     249           1 :                 logger.atFine().log(
     250             :                     "advisory deadline exceeded (%s)",
     251           1 :                     getTimeoutFormatter(advisoryDeadline.id() + ".timeout")
     252           1 :                         .apply(advisoryDeadline.timeout()));
     253           1 :                 cancellationsMetrics.countAdvisoryDeadline(requestInfo, advisoryDeadline.id());
     254             :               }
     255           1 :             });
     256             :     // remove advisory deadlines which have already been reported as exceeded so that they don't get
     257             :     // reported again for this request
     258         104 :     exceededAdvisoryDeadlines.forEach(advisoryDeadlines::remove);
     259             : 
     260         104 :     if (deadline.isPresent() && now > deadline.get()) {
     261           0 :       onCancelled.onCancel(cancellationReason, getTimeoutFormatter(timeoutName).apply(timeout));
     262             :     }
     263         104 :   }
     264             : 
     265             :   /**
     266             :    * Parses the given timeout value.
     267             :    *
     268             :    * @param timeoutValue the timeout that should be parsed, must represent a numerical time unit
     269             :    *     (e.g. "5m"), if no time unit is specified minutes are assumed, may be {@code null}
     270             :    * @return the parsed timeout in nanoseconds, {@code 0} if no timeout should be applied
     271             :    * @throws InvalidDeadlineException thrown if the provided deadline value cannot be parsed, e.g.
     272             :    *     because it uses a bad time unit
     273             :    */
     274             :   private static Optional<Long> parseTimeout(@Nullable String timeoutValue)
     275             :       throws InvalidDeadlineException {
     276         104 :     if (Strings.isNullOrEmpty(timeoutValue)) {
     277         104 :       return Optional.empty();
     278             :     }
     279             : 
     280           2 :     if ("0".equals(timeoutValue)) {
     281           2 :       return Optional.of(0L);
     282             :     }
     283             : 
     284             :     // If no time unit was specified, assume milliseconds.
     285           2 :     if (Longs.tryParse(timeoutValue) != null) {
     286           2 :       throw new InvalidDeadlineException(String.format("Missing time unit: %s", timeoutValue));
     287             :     }
     288             : 
     289             :     try {
     290           2 :       long parsedTimeout =
     291           2 :           ConfigUtil.getTimeUnit(timeoutValue, /* defaultValue= */ -1, TimeUnit.NANOSECONDS);
     292           2 :       if (parsedTimeout == -1) {
     293           2 :         throw new InvalidDeadlineException(String.format("Invalid value: %s", timeoutValue));
     294             :       }
     295           2 :       return Optional.of(parsedTimeout);
     296           2 :     } catch (IllegalArgumentException e) {
     297           2 :       throw new InvalidDeadlineException(e.getMessage(), e);
     298             :     }
     299             :   }
     300             : 
     301             :   @AutoValue
     302           2 :   abstract static class ServerDeadline {
     303             :     abstract String id();
     304             : 
     305             :     abstract long timeout();
     306             : 
     307             :     abstract boolean isAdvisory();
     308             : 
     309             :     boolean hasTimeout() {
     310           2 :       return timeout() > 0;
     311             :     }
     312             : 
     313             :     static ServerDeadline readFrom(RequestConfig requestConfig) {
     314           2 :       String timeoutValue =
     315           2 :           requestConfig.cfg().getString(requestConfig.section(), requestConfig.id(), "timeout");
     316           2 :       boolean isAdvisory =
     317             :           requestConfig
     318           2 :               .cfg()
     319           2 :               .getBoolean(
     320           2 :                   requestConfig.section(),
     321           2 :                   requestConfig.id(),
     322             :                   "isAdvisory",
     323             :                   /* defaultValue= */ false);
     324             :       try {
     325           2 :         Optional<Long> timeout = parseTimeout(timeoutValue);
     326           2 :         return new AutoValue_DeadlineChecker_ServerDeadline(
     327           2 :             requestConfig.id(), timeout.orElse(0L), isAdvisory);
     328           1 :       } catch (InvalidDeadlineException e) {
     329           1 :         logger.atWarning().log(
     330             :             "Ignoring invalid deadline configuration %s.%s.timeout: %s",
     331           1 :             requestConfig.section(), requestConfig.id(), e.getMessage());
     332           1 :         return new AutoValue_DeadlineChecker_ServerDeadline(requestConfig.id(), 0, isAdvisory);
     333             :       }
     334             :     }
     335             :   }
     336             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750