001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.util; 019 020import static org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Objects; 027import java.util.Set; 028import org.apache.hadoop.conf.Configuration; 029import org.apache.hadoop.hbase.HConstants; 030import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; 031import org.apache.hadoop.hbase.coprocessor.CoprocessorReloadTask; 032import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController; 033import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController; 034import org.apache.hadoop.hbase.security.access.MasterReadOnlyController; 035import org.apache.hadoop.hbase.security.access.RegionReadOnlyController; 036import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController; 037import org.apache.yetus.audience.InterfaceAudience; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; 042import org.apache.hbase.thirdparty.com.google.common.base.Strings; 043import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableList; 044import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap; 045 046/** 047 * Helper class for coprocessor host when configuration changes. 048 */ 049@InterfaceAudience.Private 050public final class CoprocessorConfigurationUtil { 051 private static final Logger LOG = LoggerFactory.getLogger(CoprocessorConfigurationUtil.class); 052 053 private static final ImmutableMap<String, List<String>> READONLY_COPROCESSORS = 054 ImmutableMap.of(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, 055 ImmutableList.of(MasterReadOnlyController.class.getName()), 056 CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY, 057 ImmutableList.of(RegionServerReadOnlyController.class.getName()), 058 CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, 059 ImmutableList.of(RegionReadOnlyController.class.getName(), 060 BulkLoadReadOnlyController.class.getName(), EndpointReadOnlyController.class.getName())); 061 062 private CoprocessorConfigurationUtil() { 063 } 064 065 /** 066 * Check configuration change by comparing current loaded coprocessors with configuration values. 067 * This method is useful when the configuration object has been updated, but we need to determine 068 * if the coprocessor configuration has actually changed compared to what's currently loaded. 069 * <p> 070 * <b>Note:</b> This method only detects changes in the set of coprocessor class names. It does 071 * <b>not</b> detect changes to priority or path for coprocessors that are already loaded with the 072 * same class name. If you need to update the priority or path of an existing coprocessor, you 073 * must restart the region/regionserver/master. 074 * @param coprocessorHost the coprocessor host to check current loaded coprocessors (can be null) 075 * @param conf the configuration to check 076 * @param configurationKey the configuration keys to check 077 * @return true if configuration has changed, false otherwise 078 */ 079 public static boolean checkConfigurationChange(CoprocessorHost<?, ?> coprocessorHost, 080 Configuration conf, String... configurationKey) { 081 Preconditions.checkArgument(configurationKey != null, "Configuration Key(s) must be provided"); 082 Preconditions.checkArgument(conf != null, "Configuration must be provided"); 083 084 if ( 085 !conf.getBoolean(CoprocessorHost.COPROCESSORS_ENABLED_CONF_KEY, 086 CoprocessorHost.DEFAULT_COPROCESSORS_ENABLED) 087 ) { 088 return false; 089 } 090 091 if (coprocessorHost == null) { 092 // If no coprocessor host exists, check if any coprocessors are now configured 093 return hasCoprocessorsConfigured(conf, configurationKey); 094 } 095 096 // Get currently loaded coprocessor class names 097 Set<String> currentlyLoaded = coprocessorHost.getCoprocessorClassNames(); 098 099 // Get coprocessor class names from configuration 100 // Only class names are compared; priority and path changes are not detected 101 Set<String> configuredClasses = new HashSet<>(); 102 for (String key : configurationKey) { 103 String[] classes = conf.getStrings(key); 104 if (classes != null) { 105 for (String className : classes) { 106 // Handle the className|priority|path format 107 String[] classNameToken = className.split("\\|"); 108 String actualClassName = classNameToken[0].trim(); 109 if (!Strings.isNullOrEmpty(actualClassName)) { 110 configuredClasses.add(actualClassName); 111 } 112 } 113 } 114 } 115 116 // Compare the two sets 117 return !currentlyLoaded.equals(configuredClasses); 118 } 119 120 /** 121 * Helper method to check if there are any coprocessors configured. 122 */ 123 private static boolean hasCoprocessorsConfigured(Configuration conf, String... configurationKey) { 124 for (String key : configurationKey) { 125 String[] coprocessors = conf.getStrings(key); 126 if (coprocessors != null && coprocessors.length > 0) { 127 return true; 128 } 129 } 130 return false; 131 } 132 133 private static List<String> getCoprocessorsFromConfig(Configuration conf, 134 String configurationKey) { 135 String[] existing = conf.getStrings(configurationKey); 136 return existing != null ? new ArrayList<>(Arrays.asList(existing)) : new ArrayList<>(); 137 } 138 139 public static void addCoprocessors(Configuration conf, String configurationKey, 140 List<String> coprocessorsToAdd) { 141 List<String> existing = getCoprocessorsFromConfig(conf, configurationKey); 142 143 boolean isModified = false; 144 145 for (String coprocessor : coprocessorsToAdd) { 146 if (!existing.contains(coprocessor)) { 147 existing.add(coprocessor); 148 isModified = true; 149 } 150 } 151 152 if (isModified) { 153 conf.setStrings(configurationKey, existing.toArray(new String[0])); 154 } 155 } 156 157 public static void removeCoprocessors(Configuration conf, String configurationKey, 158 List<String> coprocessorsToRemove) { 159 List<String> existing = getCoprocessorsFromConfig(conf, configurationKey); 160 161 if (existing.isEmpty()) { 162 return; 163 } 164 165 boolean isModified = false; 166 167 for (String coprocessor : coprocessorsToRemove) { 168 if (existing.contains(coprocessor)) { 169 existing.remove(coprocessor); 170 isModified = true; 171 } 172 } 173 174 if (isModified) { 175 conf.setStrings(configurationKey, existing.toArray(new String[0])); 176 } 177 } 178 179 private static List<String> getReadOnlyCoprocessors(String configurationKey) { 180 return READONLY_COPROCESSORS.get(configurationKey); 181 } 182 183 /** 184 * Updates the coprocessors in one Configuration object to match the coprocessors in the other 185 * @param srcConf the Configuration object we are getting coprocessors from 186 * @param dstConf the Configuration object we are updating 187 * @param coprocessorConfKey the type of coprocessors we are updating 188 */ 189 private static void syncCoprocessorsWithConf(Configuration srcConf, Configuration dstConf, 190 String coprocessorConfKey) { 191 String configuredCps = srcConf.get(coprocessorConfKey); 192 dstConf.set(coprocessorConfKey, Objects.requireNonNullElse(configuredCps, "")); 193 194 // A configuration with region coprocessors may also have user region coprocessors 195 if (CoprocessorHost.REGION_COPROCESSOR_CONF_KEY.equals(coprocessorConfKey)) { 196 String configuredUserCps = srcConf.get(CoprocessorHost.USER_REGION_COPROCESSOR_CONF_KEY); 197 dstConf.set(CoprocessorHost.USER_REGION_COPROCESSOR_CONF_KEY, 198 Objects.requireNonNullElse(configuredUserCps, "")); 199 } 200 } 201 202 /** 203 * This method adds or removes relevant ReadOnlyController coprocessors to the provided 204 * configuration based on whether read-only mode is enabled in the provided Configuration. 205 * @param conf The up-to-date configuration used to determine how to handle 206 * coprocessors 207 * @param coprocessorConfKey The configuration key name 208 */ 209 public static void syncReadOnlyConfigurations(Configuration conf, String coprocessorConfKey) { 210 boolean isReadOnlyModeEnabled = ConfigurationUtil.isReadOnlyModeEnabledInConf(conf); 211 212 List<String> cpList = getReadOnlyCoprocessors(coprocessorConfKey); 213 if (isReadOnlyModeEnabled) { 214 CoprocessorConfigurationUtil.addCoprocessors(conf, coprocessorConfKey, cpList); 215 } else { 216 CoprocessorConfigurationUtil.removeCoprocessors(conf, coprocessorConfKey, cpList); 217 } 218 } 219 220 /** 221 * Check whether ReadOnlyController coprocessors have been loaded in the provided configuration. 222 * @param conf the configuration we are checking 223 * @param coprocessorConfKey configuration key used for setting master, region server, or region 224 * coprocessors 225 * @return true if the ReadOnlyCoprocessors are loaded in the configuration; false otherwise 226 */ 227 public static boolean areReadOnlyCoprocessorsLoaded(Configuration conf, 228 String coprocessorConfKey) { 229 // Using a HashSet will improve performance when searching for read-only coprocessors 230 HashSet<String> allCoprocessors = 231 new HashSet<>(getCoprocessorsFromConfig(conf, coprocessorConfKey)); 232 List<String> readOnlyCoprocessors = getReadOnlyCoprocessors(coprocessorConfKey); 233 return allCoprocessors.containsAll(readOnlyCoprocessors); 234 } 235 236 /** 237 * Gets the name of a component based on the provided coprocessor configuration key. 238 * @param coprocessorConfKey configuration key used for setting master, region server, or region 239 * coprocessors 240 * @return the component type - Master, Region Server, or Region 241 */ 242 public static String getComponentName(String coprocessorConfKey) { 243 return switch (coprocessorConfKey) { 244 case CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY -> "Master"; 245 case CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY -> "Region Server"; 246 case CoprocessorHost.REGION_COPROCESSOR_CONF_KEY -> "Region"; 247 default -> throw new IllegalArgumentException( 248 "Unsupported coprocessor configuration key: " + coprocessorConfKey); 249 }; 250 } 251 252 /** 253 * This method updates the coprocessors on the master, region server, or region if a change has 254 * been detected. Detected changes include changes in coprocessors or changes in read-only mode 255 * configuration. If a change is detected, then new coprocessors are loaded using the provided 256 * reload method. The new value for the read-only config variable is updated as well. 257 * @param updatedConf an updated configuration 258 * @param originalConf the actual configuration we want to update, which may or may 259 * not be the same Configuration object as {@code updatedConf} 260 * @param originalIsReadOnlyEnabled the original value for 261 * {@value HConstants#HBASE_GLOBAL_READONLY_ENABLED_KEY} 262 * @param coprocessorHost the coprocessor host for HMaster, HRegionServer, or HRegion 263 * @param coprocessorConfKey configuration key used for setting master, region server, or 264 * region coprocessors 265 * @param isMaintenanceMode whether maintenance mode is active (mainly for HMaster) 266 * @param instance string value of the instance calling this method (mainly helps 267 * with tracking region logging) 268 * @param reloadTask lambda function that reloads coprocessors on the master, 269 * region server, or region 270 */ 271 public static void maybeUpdateCoprocessors(Configuration updatedConf, Configuration originalConf, 272 boolean originalIsReadOnlyEnabled, CoprocessorHost<?, ?> coprocessorHost, 273 String coprocessorConfKey, boolean isMaintenanceMode, String instance, 274 CoprocessorReloadTask reloadTask) { 275 276 String componentName = getComponentName(coprocessorConfKey); 277 boolean updatedReadOnlyMode = ConfigurationUtil.isReadOnlyModeEnabledInConf(updatedConf); 278 boolean hasReadOnlyModeChanged = originalIsReadOnlyEnabled != updatedReadOnlyMode; 279 boolean hasCoprocessorConfigChanged = CoprocessorConfigurationUtil 280 .checkConfigurationChange(coprocessorHost, updatedConf, coprocessorConfKey); 281 282 if ((hasCoprocessorConfigChanged || hasReadOnlyModeChanged) && !isMaintenanceMode) { 283 LOG.info("Updating coprocessors for {} {} because the configuration has changed", 284 componentName, instance); 285 // In real HBase deployments, updatedConf and originalConf reference the same Configuration 286 // object for HMaster and HRegionServer respectively. However, for HRegion and for unit test 287 // cases, these are different objects, and the original conf needs to be updated accordingly. 288 if (updatedConf != originalConf) { 289 syncCoprocessorsWithConf(updatedConf, originalConf, coprocessorConfKey); 290 originalConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, updatedReadOnlyMode); 291 } 292 // This needs to run even if read-only mode has not changed in case ReadOnly coprocessors 293 // were unintentionally added/removed in the previous code block 294 syncReadOnlyConfigurations(originalConf, coprocessorConfKey); 295 reloadTask.reload(originalConf); 296 } 297 298 if (hasReadOnlyModeChanged) { 299 LOG.info("Config {} has been dynamically changed to {} for {} {}", 300 HBASE_GLOBAL_READONLY_ENABLED_KEY, updatedReadOnlyMode, componentName, instance); 301 } 302 } 303}