Line data Source code
1 : // Copyright (C) 2019 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 com.google.common.base.Predicate; 18 : import com.google.common.base.Throwables; 19 : import com.google.common.collect.ImmutableList; 20 : import com.google.gerrit.common.Nullable; 21 : import com.google.gerrit.exceptions.MergeUpdateException; 22 : import com.google.gerrit.git.LockFailureException; 23 : import com.google.gerrit.server.project.ProjectConfig; 24 : import java.util.Optional; 25 : import org.eclipse.jgit.errors.MissingObjectException; 26 : import org.eclipse.jgit.lib.RefUpdate; 27 : 28 : /** 29 : * Class to detect and handle exceptions that are caused by temporary errors, and hence should cause 30 : * a retry of the failed operation. 31 : */ 32 39 : public class ExceptionHookImpl implements ExceptionHook { 33 : private static final String LOCK_FAILURE_USER_MESSAGE = 34 : "Updating a ref failed with LOCK_FAILURE.\n" 35 : + "This may be a temporary issue due to concurrent updates.\n" 36 : + "Please retry later."; 37 : private static final String INVALID_PROJECT_CONFIG_USER_MESSAGE = 38 : "Invalid " + ProjectConfig.PROJECT_CONFIG + " file."; 39 : private static final String CONTACT_PROJECT_OWNER_USER_MESSAGE = 40 : "Please contact the project owner."; 41 : 42 : @Override 43 : public boolean shouldRetry(String actionType, String actionName, Throwable throwable) { 44 38 : return isLockFailure(throwable); 45 : } 46 : 47 : @Override 48 : public boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) { 49 1 : return isInvalidProjectConfig(throwable); 50 : } 51 : 52 : @Override 53 : public Optional<String> formatCause(Throwable throwable) { 54 30 : if (isLockFailure(throwable)) { 55 11 : return Optional.of(RefUpdate.Result.LOCK_FAILURE.name()); 56 : } 57 21 : if (isMissingObjectException(throwable)) { 58 0 : return Optional.of("missing_object"); 59 : } 60 21 : if (isInvalidProjectConfig(throwable)) { 61 1 : return Optional.of("invalid_project_config"); 62 : } 63 20 : return Optional.empty(); 64 : } 65 : 66 : @Override 67 : public ImmutableList<String> getUserMessages(Throwable throwable, @Nullable String traceId) { 68 2 : if (isLockFailure(throwable)) { 69 0 : return ImmutableList.of(LOCK_FAILURE_USER_MESSAGE); 70 : } 71 2 : if (isInvalidProjectConfig(throwable)) { 72 1 : return ImmutableList.of( 73 1 : getInvalidConfigMessage(throwable).orElse(INVALID_PROJECT_CONFIG_USER_MESSAGE) 74 : + "\n" 75 : + CONTACT_PROJECT_OWNER_USER_MESSAGE); 76 : } 77 1 : if (throwable instanceof MergeUpdateException) { 78 0 : return ImmutableList.of(throwable.getMessage()); 79 : } 80 1 : return ImmutableList.of(); 81 : } 82 : 83 : @Override 84 : public Optional<Status> getStatus(Throwable throwable) { 85 5 : if (isLockFailure(throwable)) { 86 0 : return Optional.of(Status.create(503, "Lock failure")); 87 : } 88 5 : if (isInvalidProjectConfig(throwable)) { 89 1 : return Optional.of(Status.create(409, "Conflict")); 90 : } 91 4 : return Optional.empty(); 92 : } 93 : 94 : private static boolean isLockFailure(Throwable throwable) { 95 39 : return isMatching(throwable, t -> t instanceof LockFailureException); 96 : } 97 : 98 : private static boolean isMissingObjectException(Throwable throwable) { 99 21 : return isMatching(throwable, t -> t instanceof MissingObjectException); 100 : } 101 : 102 : private static boolean isInvalidProjectConfig(Throwable throwable) { 103 21 : return isMatching( 104 : throwable, 105 : t -> 106 21 : t instanceof InvalidConfigFileException 107 21 : && ProjectConfig.PROJECT_CONFIG.equals( 108 1 : ((InvalidConfigFileException) t).getFileName())); 109 : } 110 : 111 : private Optional<String> getInvalidConfigMessage(Throwable throwable) { 112 1 : return Throwables.getCausalChain(throwable).stream() 113 1 : .filter(InvalidConfigFileException.class::isInstance) 114 1 : .map(ex -> ex.getMessage()) 115 1 : .findFirst(); 116 : } 117 : 118 : /** 119 : * Check whether the given exception or any of its causes matches the given predicate. 120 : * 121 : * @param throwable Exception that should be tested 122 : * @param predicate predicate to check if a throwable matches 123 : * @return {@code true} if the given exception or any of its causes matches the given predicate 124 : */ 125 : private static boolean isMatching(Throwable throwable, Predicate<Throwable> predicate) { 126 39 : return Throwables.getCausalChain(throwable).stream().anyMatch(predicate); 127 : } 128 : }