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.quotas;
019
020import java.io.IOException;
021import java.util.Objects;
022import java.util.concurrent.TimeUnit;
023
024import org.apache.hadoop.hbase.TableName;
025import org.apache.hadoop.hbase.quotas.QuotaSettingsFactory.QuotaGlobalsSettingsBypass;
026import org.apache.yetus.audience.InterfaceAudience;
027
028import org.apache.hbase.thirdparty.com.google.protobuf.TextFormat;
029import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
030import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaRequest;
031
032@InterfaceAudience.Public
033public abstract class QuotaSettings {
034  private final String userName;
035  private final String namespace;
036  private final TableName tableName;
037  private final String regionServer;
038
039  protected QuotaSettings(final String userName, final TableName tableName, final String namespace,
040      final String regionServer) {
041    this.userName = userName;
042    this.namespace = namespace;
043    this.tableName = tableName;
044    this.regionServer = regionServer;
045  }
046
047  public abstract QuotaType getQuotaType();
048
049  public String getUserName() {
050    return userName;
051  }
052
053  public TableName getTableName() {
054    return tableName;
055  }
056
057  public String getNamespace() {
058    return namespace;
059  }
060
061  public String getRegionServer() {
062    return regionServer;
063  }
064
065  /**
066   * Converts the protocol buffer request into a QuotaSetting POJO. Arbitrarily
067   * enforces that the request only contain one "limit", despite the message
068   * allowing multiple. The public API does not allow such use of the message.
069   *
070   * @param request The protocol buffer request.
071   * @return A {@link QuotaSettings} POJO.
072   */
073  @InterfaceAudience.Private
074  public static QuotaSettings buildFromProto(SetQuotaRequest request) {
075    String username = null;
076    if (request.hasUserName()) {
077      username = request.getUserName();
078    }
079    TableName tableName = null;
080    if (request.hasTableName()) {
081      tableName = ProtobufUtil.toTableName(request.getTableName());
082    }
083    String namespace = null;
084    if (request.hasNamespace()) {
085      namespace = request.getNamespace();
086    }
087    String regionServer = null;
088    if (request.hasRegionServer()) {
089      regionServer = request.getRegionServer();
090    }
091    if (request.hasBypassGlobals()) {
092      // Make sure we don't have either of the two below limits also included
093      if (request.hasSpaceLimit() || request.hasThrottle()) {
094        throw new IllegalStateException(
095            "SetQuotaRequest has multiple limits: " + TextFormat.shortDebugString(request));
096      }
097      return new QuotaGlobalsSettingsBypass(
098          username, tableName, namespace, regionServer, request.getBypassGlobals());
099    } else if (request.hasSpaceLimit()) {
100      // Make sure we don't have the below limit as well
101      if (request.hasThrottle()) {
102        throw new IllegalStateException(
103            "SetQuotaRequests has multiple limits: " + TextFormat.shortDebugString(request));
104      }
105      // Sanity check on the pb received.
106      if (!request.getSpaceLimit().hasQuota()) {
107        throw new IllegalArgumentException(
108            "SpaceLimitRequest is missing the expected SpaceQuota.");
109      }
110      return QuotaSettingsFactory.fromSpace(
111          tableName, namespace, request.getSpaceLimit().getQuota());
112    } else if (request.hasThrottle()) {
113      return new ThrottleSettings(username, tableName, namespace, regionServer,
114          request.getThrottle());
115    } else {
116      throw new IllegalStateException("Unhandled SetRequestRequest state");
117    }
118  }
119
120  /**
121   * Convert a QuotaSettings to a protocol buffer SetQuotaRequest.
122   * This is used internally by the Admin client to serialize the quota settings
123   * and send them to the master.
124   */
125  @InterfaceAudience.Private
126  public static SetQuotaRequest buildSetQuotaRequestProto(final QuotaSettings settings) {
127    SetQuotaRequest.Builder builder = SetQuotaRequest.newBuilder();
128    if (settings.getUserName() != null) {
129      builder.setUserName(settings.getUserName());
130    }
131    if (settings.getTableName() != null) {
132      builder.setTableName(ProtobufUtil.toProtoTableName(settings.getTableName()));
133    }
134    if (settings.getNamespace() != null) {
135      builder.setNamespace(settings.getNamespace());
136    }
137    if (settings.getRegionServer() != null) {
138      builder.setRegionServer(settings.getRegionServer());
139    }
140    settings.setupSetQuotaRequest(builder);
141    return builder.build();
142  }
143
144  /**
145   * Called by toSetQuotaRequestProto()
146   * the subclass should implement this method to set the specific SetQuotaRequest
147   * properties.
148   */
149  @InterfaceAudience.Private
150  protected abstract void setupSetQuotaRequest(SetQuotaRequest.Builder builder);
151
152  protected String ownerToString() {
153    StringBuilder builder = new StringBuilder();
154    if (userName != null) {
155      builder.append("USER => '");
156      builder.append(userName);
157      builder.append("', ");
158    }
159    if (tableName != null) {
160      builder.append("TABLE => '");
161      builder.append(tableName.toString());
162      builder.append("', ");
163    }
164    if (namespace != null) {
165      builder.append("NAMESPACE => '");
166      builder.append(namespace);
167      builder.append("', ");
168    }
169    if (regionServer != null) {
170      builder.append("REGIONSERVER => ").append(regionServer).append(", ");
171    }
172    return builder.toString();
173  }
174
175  protected static String sizeToString(final long size) {
176    if (size >= (1L << 50)) {
177      return String.format("%.2fP", (double)size / (1L << 50));
178    }
179    if (size >= (1L << 40)) {
180      return String.format("%.2fT", (double)size / (1L << 40));
181    }
182    if (size >= (1L << 30)) {
183      return String.format("%.2fG", (double)size / (1L << 30));
184    }
185    if (size >= (1L << 20)) {
186      return String.format("%.2fM", (double)size / (1L << 20));
187    }
188    if (size >= (1L << 10)) {
189      return String.format("%.2fK", (double)size / (1L << 10));
190    }
191    return String.format("%.2fB", (double)size);
192  }
193
194  protected static String timeToString(final TimeUnit timeUnit) {
195    switch (timeUnit) {
196      case NANOSECONDS:  return "nsec";
197      case MICROSECONDS: return "usec";
198      case MILLISECONDS: return "msec";
199      case SECONDS:      return "sec";
200      case MINUTES:      return "min";
201      case HOURS:        return "hour";
202      case DAYS:         return "day";
203    }
204    throw new RuntimeException("Invalid TimeUnit " + timeUnit);
205  }
206
207  /**
208   * Merges the provided settings with {@code this} and returns a new settings
209   * object to the caller if the merged settings differ from the original.
210   *
211   * @param newSettings The new settings to merge in.
212   * @return The merged {@link QuotaSettings} object or null if the quota should be deleted.
213   */
214  abstract QuotaSettings merge(QuotaSettings newSettings) throws IOException;
215
216  /**
217   * Validates that settings being merged into {@code this} is targeting the same "subject", e.g.
218   * user, table, namespace.
219   *
220   * @param mergee The quota settings to be merged into {@code this}.
221   * @throws IllegalArgumentException if the subjects are not equal.
222   */
223  void validateQuotaTarget(QuotaSettings mergee) {
224    if (!Objects.equals(getUserName(), mergee.getUserName())) {
225      throw new IllegalArgumentException("Mismatched user names on settings to merge");
226    }
227    if (!Objects.equals(getTableName(), mergee.getTableName())) {
228      throw new IllegalArgumentException("Mismatched table names on settings to merge");
229    }
230    if (!Objects.equals(getNamespace(), mergee.getNamespace())) {
231      throw new IllegalArgumentException("Mismatched namespace on settings to merge");
232    }
233    if (!Objects.equals(getRegionServer(), mergee.getRegionServer())) {
234      throw new IllegalArgumentException("Mismatched region server on settings to merge");
235    }
236  }
237}