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.master.http; 019 020import java.io.UnsupportedEncodingException; 021import java.net.URLDecoder; 022import java.net.URLEncoder; 023import java.nio.charset.StandardCharsets; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Iterator; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.stream.StreamSupport; 030import javax.servlet.http.HttpServletRequest; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.lang3.builder.ToStringBuilder; 033import org.apache.commons.lang3.builder.ToStringStyle; 034import org.apache.hadoop.hbase.CompareOperator; 035import org.apache.hadoop.hbase.HConstants; 036import org.apache.hadoop.hbase.TableName; 037import org.apache.hadoop.hbase.client.AdvancedScanResultConsumer; 038import org.apache.hadoop.hbase.client.AsyncConnection; 039import org.apache.hadoop.hbase.client.AsyncTable; 040import org.apache.hadoop.hbase.client.ResultScanner; 041import org.apache.hadoop.hbase.client.Scan; 042import org.apache.hadoop.hbase.filter.Filter; 043import org.apache.hadoop.hbase.filter.FilterList; 044import org.apache.hadoop.hbase.filter.PrefixFilter; 045import org.apache.hadoop.hbase.filter.SingleColumnValueFilter; 046import org.apache.hadoop.hbase.master.RegionState; 047import org.apache.hadoop.hbase.util.Bytes; 048import org.apache.yetus.audience.InterfaceAudience; 049 050import org.apache.hbase.thirdparty.com.google.common.collect.Iterators; 051import org.apache.hbase.thirdparty.io.netty.handler.codec.http.QueryStringEncoder; 052 053/** 054 * <p> 055 * Support class for the "Meta Entries" section in {@code resources/hbase-webapps/master/table.jsp}. 056 * </p> 057 * <p> 058 * <b>Interface</b>. This class's intended consumer is {@code table.jsp}. As such, it's primary 059 * interface is the active {@link HttpServletRequest}, from which it uses the {@code scan_*} request 060 * parameters. This class supports paging through an optionally filtered view of the contents of 061 * {@code hbase:meta}. Those filters and the pagination offset are specified via these request 062 * parameters. It provides helper methods for constructing pagination links. 063 * <ul> 064 * <li>{@value #NAME_PARAM} - the name of the table requested. The only table of our concern here is 065 * {@code hbase:meta}; any other value is effectively ignored by the giant conditional in the 066 * jsp.</li> 067 * <li>{@value #SCAN_LIMIT_PARAM} - specifies a limit on the number of region (replicas) rendered on 068 * the by the table in a single request -- a limit on page size. This corresponds to the number of 069 * {@link RegionReplicaInfo} objects produced by {@link Results#iterator()}. When a value for 070 * {@code scan_limit} is invalid or not specified, the default value of {@value #SCAN_LIMIT_DEFAULT} 071 * is used. In order to avoid excessive resource consumption, a maximum value of 072 * {@value #SCAN_LIMIT_MAX} is enforced.</li> 073 * <li>{@value #SCAN_REGION_STATE_PARAM} - an optional filter on {@link RegionState}.</li> 074 * <li>{@value #SCAN_START_PARAM} - specifies the rowkey at which a scan should start. For usage 075 * details, see the below section on <b>Pagination</b>.</li> 076 * <li>{@value #SCAN_TABLE_PARAM} - specifies a filter on the values returned, limiting them to 077 * regions from a specified table. This parameter is implemented as a prefix filter on the 078 * {@link Scan}, so in effect it can be used for simple namespace and multi-table matches.</li> 079 * </ul> 080 * </p> 081 * <p> 082 * <b>Pagination</b>. A single page of results are made available via {@link #getResults()} / an 083 * instance of {@link Results}. Callers use its {@link Iterator} consume the page of 084 * {@link RegionReplicaInfo} instances, each of which represents a region or region replica. Helper 085 * methods are provided for building page navigation controls preserving the user's selected filter 086 * set: {@link #buildFirstPageUrl()}, {@link #buildNextPageUrl(byte[])}. Pagination is implemented 087 * using a simple offset + limit system. Offset is provided by the {@value #SCAN_START_PARAM}, limit 088 * via {@value #SCAN_LIMIT_PARAM}. Under the hood, the {@link Scan} is constructed with 089 * {@link Scan#setMaxResultSize(long)} set to ({@value SCAN_LIMIT_PARAM} +1), while the 090 * {@link Results} {@link Iterator} honors {@value #SCAN_LIMIT_PARAM}. The +1 allows the caller to 091 * know if a "next page" is available via {@link Results#hasMoreResults()}. Note that this 092 * pagination strategy is incomplete when it comes to region replicas and can potentially omit 093 * rendering replicas that fall between the last rowkey offset and {@code replicaCount % page size}. 094 * </p> 095 * <p> 096 * <b>Error Messages</b>. Any time there's an error parsing user input, a message will be populated 097 * in {@link #getErrorMessages()}. Any fields which produce an error will have their filter values 098 * set to the default, except for a value of {@value #SCAN_LIMIT_PARAM} that exceeds 099 * {@value #SCAN_LIMIT_MAX}, in which case {@value #SCAN_LIMIT_MAX} is used. 100 * </p> 101 */ 102@InterfaceAudience.Private 103public class MetaBrowser { 104 public static final String NAME_PARAM = "name"; 105 public static final String SCAN_LIMIT_PARAM = "scan_limit"; 106 public static final String SCAN_REGION_STATE_PARAM = "scan_region_state"; 107 public static final String SCAN_START_PARAM = "scan_start"; 108 public static final String SCAN_TABLE_PARAM = "scan_table"; 109 110 public static final int SCAN_LIMIT_DEFAULT = 10; 111 public static final int SCAN_LIMIT_MAX = 10_000; 112 113 private final AsyncConnection connection; 114 private final HttpServletRequest request; 115 private final List<String> errorMessages; 116 private final String name; 117 private final Integer scanLimit; 118 private final RegionState.State scanRegionState; 119 private final byte[] scanStart; 120 private final TableName scanTable; 121 122 public MetaBrowser(final AsyncConnection connection, final HttpServletRequest request) { 123 this.connection = connection; 124 this.request = request; 125 this.errorMessages = new LinkedList<>(); 126 this.name = resolveName(request); 127 this.scanLimit = resolveScanLimit(request); 128 this.scanRegionState = resolveScanRegionState(request); 129 this.scanStart = resolveScanStart(request); 130 this.scanTable = resolveScanTable(request); 131 } 132 133 public List<String> getErrorMessages() { 134 return errorMessages; 135 } 136 137 public String getName() { 138 return name; 139 } 140 141 public Integer getScanLimit() { 142 return scanLimit; 143 } 144 145 public byte[] getScanStart() { 146 return scanStart; 147 } 148 149 public RegionState.State getScanRegionState() { 150 return scanRegionState; 151 } 152 153 public TableName getScanTable() { 154 return scanTable; 155 } 156 157 public Results getResults() { 158 final AsyncTable<AdvancedScanResultConsumer> asyncTable = 159 connection.getTable(TableName.META_TABLE_NAME); 160 return new Results(asyncTable.getScanner(buildScan())); 161 } 162 163 @Override 164 public String toString() { 165 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 166 .append("scanStart", scanStart).append("scanLimit", scanLimit).append("scanTable", scanTable) 167 .append("scanRegionState", scanRegionState).toString(); 168 } 169 170 private static String resolveName(final HttpServletRequest request) { 171 return resolveRequestParameter(request, NAME_PARAM); 172 } 173 174 private Integer resolveScanLimit(final HttpServletRequest request) { 175 final String requestValueStr = resolveRequestParameter(request, SCAN_LIMIT_PARAM); 176 if (StringUtils.isBlank(requestValueStr)) { 177 return null; 178 } 179 180 final Integer requestValue = tryParseInt(requestValueStr); 181 if (requestValue == null) { 182 errorMessages.add(buildScanLimitMalformedErrorMessage(requestValueStr)); 183 return null; 184 } 185 if (requestValue <= 0) { 186 errorMessages.add(buildScanLimitLTEQZero(requestValue)); 187 return SCAN_LIMIT_DEFAULT; 188 } 189 190 final int truncatedValue = Math.min(requestValue, SCAN_LIMIT_MAX); 191 if (requestValue != truncatedValue) { 192 errorMessages.add(buildScanLimitExceededErrorMessage(requestValue)); 193 } 194 return truncatedValue; 195 } 196 197 private RegionState.State resolveScanRegionState(final HttpServletRequest request) { 198 final String requestValueStr = resolveRequestParameter(request, SCAN_REGION_STATE_PARAM); 199 if (requestValueStr == null) { 200 return null; 201 } 202 final RegionState.State requestValue = tryValueOf(RegionState.State.class, requestValueStr); 203 if (requestValue == null) { 204 errorMessages.add(buildScanRegionStateMalformedErrorMessage(requestValueStr)); 205 return null; 206 } 207 return requestValue; 208 } 209 210 private static byte[] resolveScanStart(final HttpServletRequest request) { 211 // TODO: handle replicas that fall between the last rowkey and pagination limit. 212 final String requestValue = resolveRequestParameter(request, SCAN_START_PARAM); 213 if (requestValue == null) { 214 return null; 215 } 216 return Bytes.toBytesBinary(requestValue); 217 } 218 219 private static TableName resolveScanTable(final HttpServletRequest request) { 220 final String requestValue = resolveRequestParameter(request, SCAN_TABLE_PARAM); 221 if (requestValue == null) { 222 return null; 223 } 224 return TableName.valueOf(requestValue); 225 } 226 227 private static String resolveRequestParameter(final HttpServletRequest request, 228 final String param) { 229 if (request == null) { 230 return null; 231 } 232 final String requestValueStrEnc = request.getParameter(param); 233 if (StringUtils.isBlank(requestValueStrEnc)) { 234 return null; 235 } 236 return urlDecode(requestValueStrEnc); 237 } 238 239 private static Filter buildTableFilter(final TableName tableName) { 240 return new PrefixFilter(tableName.toBytes()); 241 } 242 243 private static Filter buildScanRegionStateFilter(final RegionState.State state) { 244 return new SingleColumnValueFilter(HConstants.CATALOG_FAMILY, HConstants.STATE_QUALIFIER, 245 CompareOperator.EQUAL, 246 // use the same serialization strategy as found in MetaTableAccessor#addRegionStateToPut 247 Bytes.toBytes(state.name())); 248 } 249 250 private Filter buildScanFilter() { 251 if (scanTable == null && scanRegionState == null) { 252 return null; 253 } 254 255 final List<Filter> filters = new ArrayList<>(2); 256 if (scanTable != null) { 257 filters.add(buildTableFilter(scanTable)); 258 } 259 if (scanRegionState != null) { 260 filters.add(buildScanRegionStateFilter(scanRegionState)); 261 } 262 if (filters.size() == 1) { 263 return filters.get(0); 264 } 265 return new FilterList(FilterList.Operator.MUST_PASS_ALL, filters); 266 } 267 268 private Scan buildScan() { 269 final Scan metaScan = new Scan().addFamily(HConstants.CATALOG_FAMILY).readVersions(1) 270 .setLimit((scanLimit != null ? scanLimit : SCAN_LIMIT_DEFAULT) + 1); 271 if (scanStart != null) { 272 metaScan.withStartRow(scanStart, false); 273 } 274 final Filter filter = buildScanFilter(); 275 if (filter != null) { 276 metaScan.setFilter(filter); 277 } 278 return metaScan; 279 } 280 281 /** 282 * Adds {@code value} to {@code encoder} under {@code paramName} when {@code value} is non-null. 283 */ 284 private void addParam(final QueryStringEncoder encoder, final String paramName, 285 final Object value) { 286 if (value != null) { 287 encoder.addParam(paramName, value.toString()); 288 } 289 } 290 291 private QueryStringEncoder buildFirstPageEncoder() { 292 final QueryStringEncoder encoder = new QueryStringEncoder(request.getRequestURI()); 293 addParam(encoder, NAME_PARAM, name); 294 addParam(encoder, SCAN_LIMIT_PARAM, scanLimit); 295 addParam(encoder, SCAN_REGION_STATE_PARAM, scanRegionState); 296 addParam(encoder, SCAN_TABLE_PARAM, scanTable); 297 return encoder; 298 } 299 300 public String buildFirstPageUrl() { 301 return buildFirstPageEncoder().toString(); 302 } 303 304 static String buildStartParamFrom(final byte[] lastRow) { 305 if (lastRow == null) { 306 return null; 307 } 308 return urlEncode(Bytes.toStringBinary(lastRow)); 309 } 310 311 public String buildNextPageUrl(final byte[] lastRow) { 312 final QueryStringEncoder encoder = buildFirstPageEncoder(); 313 final String startRow = buildStartParamFrom(lastRow); 314 addParam(encoder, SCAN_START_PARAM, startRow); 315 return encoder.toString(); 316 } 317 318 private static String urlEncode(final String val) { 319 if (StringUtils.isEmpty(val)) { 320 return null; 321 } 322 try { 323 return URLEncoder.encode(val, StandardCharsets.UTF_8.toString()); 324 } catch (UnsupportedEncodingException e) { 325 return null; 326 } 327 } 328 329 private static String urlDecode(final String val) { 330 if (StringUtils.isEmpty(val)) { 331 return null; 332 } 333 try { 334 return URLDecoder.decode(val, StandardCharsets.UTF_8.toString()); 335 } catch (UnsupportedEncodingException e) { 336 return null; 337 } 338 } 339 340 private static Integer tryParseInt(final String val) { 341 if (StringUtils.isEmpty(val)) { 342 return null; 343 } 344 try { 345 return Integer.parseInt(val); 346 } catch (NumberFormatException e) { 347 return null; 348 } 349 } 350 351 private static <T extends Enum<T>> T tryValueOf(final Class<T> clazz, final String value) { 352 if (clazz == null || value == null) { 353 return null; 354 } 355 try { 356 return Enum.valueOf(clazz, value); 357 } catch (IllegalArgumentException e) { 358 return null; 359 } 360 } 361 362 private static String buildScanLimitExceededErrorMessage(final int requestValue) { 363 return String.format("Requested SCAN_LIMIT value %d exceeds maximum value %d.", requestValue, 364 SCAN_LIMIT_MAX); 365 } 366 367 private static String buildScanLimitMalformedErrorMessage(final String requestValue) { 368 return String.format("Requested SCAN_LIMIT value '%s' cannot be parsed as an integer.", 369 requestValue); 370 } 371 372 private static String buildScanLimitLTEQZero(final int requestValue) { 373 return String.format("Requested SCAN_LIMIT value %d is <= 0.", requestValue); 374 } 375 376 private static String buildScanRegionStateMalformedErrorMessage(final String requestValue) { 377 return String.format( 378 "Requested SCAN_REGION_STATE value '%s' cannot be parsed as a RegionState.", requestValue); 379 } 380 381 /** 382 * Encapsulates the results produced by this {@link MetaBrowser} instance. 383 */ 384 public final class Results implements AutoCloseable, Iterable<RegionReplicaInfo> { 385 386 private final ResultScanner resultScanner; 387 private final Iterator<RegionReplicaInfo> sourceIterator; 388 389 private Results(final ResultScanner resultScanner) { 390 this.resultScanner = resultScanner; 391 this.sourceIterator = StreamSupport.stream(resultScanner.spliterator(), false) 392 .map(RegionReplicaInfo::from).flatMap(Collection::stream).iterator(); 393 } 394 395 /** 396 * @return {@code true} when the underlying {@link ResultScanner} is not yet exhausted, 397 * {@code false} otherwise. 398 */ 399 public boolean hasMoreResults() { 400 return sourceIterator.hasNext(); 401 } 402 403 @Override 404 public void close() { 405 if (resultScanner != null) { 406 resultScanner.close(); 407 } 408 } 409 410 @Override 411 public Iterator<RegionReplicaInfo> iterator() { 412 return Iterators.limit(sourceIterator, scanLimit != null ? scanLimit : SCAN_LIMIT_DEFAULT); 413 } 414 } 415}