001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to you under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.hadoop.hbase.quotas;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Map.Entry;
026
027import org.apache.hadoop.hbase.DoNotRetryIOException;
028import org.apache.hadoop.hbase.TableName;
029import org.apache.hadoop.hbase.quotas.QuotaSettingsFactory.QuotaGlobalsSettingsBypass;
030import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
031import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
032import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Quotas;
033import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.SpaceQuota;
034import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.Throttle;
035import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.TimedQuota;
036import org.apache.yetus.audience.InterfaceAudience;
037
038/**
039 * Implementation of {@link GlobalQuotaSettings} to hide the Protobuf messages we use internally.
040 */
041@InterfaceAudience.Private
042public class GlobalQuotaSettingsImpl extends GlobalQuotaSettings {
043
044  private final QuotaProtos.Throttle throttleProto;
045  private final Boolean bypassGlobals;
046  private final QuotaProtos.SpaceQuota spaceProto;
047
048  protected GlobalQuotaSettingsImpl(
049      String username, TableName tableName, String namespace, QuotaProtos.Quotas quotas) {
050    this(username, tableName, namespace,
051        (quotas != null && quotas.hasThrottle() ? quotas.getThrottle() : null),
052        (quotas != null && quotas.hasBypassGlobals() ? quotas.getBypassGlobals() : null),
053        (quotas != null && quotas.hasSpace() ? quotas.getSpace() : null));
054  }
055
056  protected GlobalQuotaSettingsImpl(
057      String userName, TableName tableName, String namespace, QuotaProtos.Throttle throttleProto,
058      Boolean bypassGlobals, QuotaProtos.SpaceQuota spaceProto) {
059    super(userName, tableName, namespace);
060    this.throttleProto = throttleProto;
061    this.bypassGlobals = bypassGlobals;
062    this.spaceProto = spaceProto;
063  }
064
065  @Override
066  public List<QuotaSettings> getQuotaSettings() {
067    // Very similar to QuotaSettingsFactory
068    List<QuotaSettings> settings = new ArrayList<>();
069    if (throttleProto != null) {
070      settings.addAll(QuotaSettingsFactory.fromThrottle(
071          getUserName(), getTableName(), getNamespace(), throttleProto));
072    }
073    if (bypassGlobals != null && bypassGlobals.booleanValue()) {
074      settings.add(new QuotaGlobalsSettingsBypass(
075          getUserName(), getTableName(), getNamespace(), true));
076    }
077    if (spaceProto != null) {
078      settings.add(QuotaSettingsFactory.fromSpace(getTableName(), getNamespace(), spaceProto));
079    }
080    return settings;
081  }
082
083  protected QuotaProtos.Throttle getThrottleProto() {
084    return this.throttleProto;
085  }
086
087  protected Boolean getBypassGlobals() {
088    return this.bypassGlobals;
089  }
090
091  protected QuotaProtos.SpaceQuota getSpaceProto() {
092    return this.spaceProto;
093  }
094
095  /**
096   * Constructs a new {@link Quotas} message from {@code this}.
097   */
098  protected Quotas toQuotas() {
099    QuotaProtos.Quotas.Builder builder = QuotaProtos.Quotas.newBuilder();
100    if (getThrottleProto() != null) {
101      builder.setThrottle(getThrottleProto());
102    }
103    if (getBypassGlobals() != null) {
104      builder.setBypassGlobals(getBypassGlobals());
105    }
106    if (getSpaceProto() != null) {
107      builder.setSpace(getSpaceProto());
108    }
109    return builder.build();
110  }
111
112  @Override
113  protected GlobalQuotaSettingsImpl merge(QuotaSettings other) throws IOException {
114    // Validate the quota subject
115    validateQuotaTarget(other);
116
117    // Propagate the Throttle
118    QuotaProtos.Throttle.Builder throttleBuilder =
119        throttleProto == null ? null : throttleProto.toBuilder();
120
121    if (other instanceof ThrottleSettings) {
122      ThrottleSettings otherThrottle = (ThrottleSettings) other;
123      if (!otherThrottle.proto.hasType() || !otherThrottle.proto.hasTimedQuota()) {
124        // It means it's a remove request
125        // To prevent the "empty" row in QuotaTableUtil.QUOTA_TABLE_NAME
126        throttleBuilder = null;
127      } else {
128        QuotaProtos.ThrottleRequest otherProto = otherThrottle.proto;
129        validateTimedQuota(otherProto.getTimedQuota());
130        if (throttleBuilder == null) {
131          throttleBuilder = QuotaProtos.Throttle.newBuilder();
132        }
133
134        switch (otherProto.getType()) {
135          case REQUEST_NUMBER:
136            throttleBuilder.setReqNum(otherProto.getTimedQuota());
137            break;
138          case REQUEST_SIZE:
139            throttleBuilder.setReqSize(otherProto.getTimedQuota());
140            break;
141          case WRITE_NUMBER:
142            throttleBuilder.setWriteNum(otherProto.getTimedQuota());
143            break;
144          case WRITE_SIZE:
145            throttleBuilder.setWriteSize(otherProto.getTimedQuota());
146            break;
147          case READ_NUMBER:
148            throttleBuilder.setReadNum(otherProto.getTimedQuota());
149            break;
150          case READ_SIZE:
151            throttleBuilder.setReadSize(otherProto.getTimedQuota());
152            break;
153        }
154      }
155    }
156
157    // Propagate the space quota portion
158    QuotaProtos.SpaceQuota.Builder spaceBuilder = (spaceProto == null
159        ? null : spaceProto.toBuilder());
160    if (other instanceof SpaceLimitSettings) {
161      if (spaceBuilder == null) {
162        spaceBuilder = QuotaProtos.SpaceQuota.newBuilder();
163      }
164      SpaceLimitSettings settingsToMerge = (SpaceLimitSettings) other;
165
166      QuotaProtos.SpaceLimitRequest spaceRequest = settingsToMerge.getProto();
167
168      // The message contained the expect SpaceQuota object
169      if (spaceRequest.hasQuota()) {
170        SpaceQuota quotaToMerge = spaceRequest.getQuota();
171        // Validate that the two settings are for the same target.
172        // SpaceQuotas either apply to a table or a namespace (no user spacequota).
173        if (!Objects.equals(getTableName(), settingsToMerge.getTableName())
174            && !Objects.equals(getNamespace(), settingsToMerge.getNamespace())) {
175          throw new IllegalArgumentException(
176              "Cannot merge " + settingsToMerge + " into " + this);
177        }
178
179        if (quotaToMerge.getRemove()) {
180          // It means it's a remove request
181          // Update the builder to propagate the removal
182          spaceBuilder.setRemove(true).clearSoftLimit().clearViolationPolicy();
183        } else {
184          // Add the new settings to the existing settings
185          spaceBuilder.mergeFrom(quotaToMerge);
186        }
187      }
188    }
189
190    boolean removeSpaceBuilder =
191        (spaceBuilder == null) || (spaceBuilder.hasRemove() && spaceBuilder.getRemove());
192
193    Boolean bypassGlobals = this.bypassGlobals;
194    if (other instanceof QuotaGlobalsSettingsBypass) {
195      bypassGlobals = ((QuotaGlobalsSettingsBypass) other).getBypass();
196    }
197
198    if (throttleBuilder == null && removeSpaceBuilder && bypassGlobals == null) {
199      return null;
200    }
201
202    return new GlobalQuotaSettingsImpl(
203        getUserName(), getTableName(), getNamespace(),
204        (throttleBuilder == null ? null : throttleBuilder.build()), bypassGlobals,
205        (removeSpaceBuilder ? null : spaceBuilder.build()));
206  }
207
208  private void validateTimedQuota(final TimedQuota timedQuota) throws IOException {
209    if (timedQuota.getSoftLimit() < 1) {
210      throw new DoNotRetryIOException(new UnsupportedOperationException(
211          "The throttle limit must be greater then 0, got " + timedQuota.getSoftLimit()));
212    }
213  }
214
215  @Override
216  public String toString() {
217    StringBuilder builder = new StringBuilder();
218    builder.append("GlobalQuota: ");
219    if (throttleProto != null) {
220      Map<ThrottleType,TimedQuota> throttleQuotas = buildThrottleQuotas(throttleProto);
221      builder.append(" { TYPE => THROTTLE ");
222      for (Entry<ThrottleType,TimedQuota> entry : throttleQuotas.entrySet()) {
223        final ThrottleType type = entry.getKey();
224        final TimedQuota timedQuota = entry.getValue();
225        builder.append("{THROTTLE_TYPE => ").append(type.name()).append(", LIMIT => ");
226        if (timedQuota.hasSoftLimit()) {
227          switch (type) {
228            case REQUEST_NUMBER:
229            case WRITE_NUMBER:
230            case READ_NUMBER:
231              builder.append(String.format("%dreq", timedQuota.getSoftLimit()));
232              break;
233            case REQUEST_SIZE:
234            case WRITE_SIZE:
235            case READ_SIZE:
236              builder.append(sizeToString(timedQuota.getSoftLimit()));
237              break;
238          }
239        } else if (timedQuota.hasShare()) {
240          builder.append(String.format("%.2f%%", timedQuota.getShare()));
241        }
242        builder.append('/');
243        builder.append(timeToString(ProtobufUtil.toTimeUnit(timedQuota.getTimeUnit())));
244        if (timedQuota.hasScope()) {
245          builder.append(", SCOPE => ");
246          builder.append(timedQuota.getScope().toString());
247        }
248      }
249      builder.append( "} } ");
250    } else {
251      builder.append(" {} ");
252    }
253    if (bypassGlobals != null) {
254      builder.append(" { GLOBAL_BYPASS => " + bypassGlobals + " } ");
255    }
256    if (spaceProto != null) {
257      builder.append(" { TYPE => SPACE");
258      if (getTableName() != null) {
259        builder.append(", TABLE => ").append(getTableName());
260      }
261      if (getNamespace() != null) {
262        builder.append(", NAMESPACE => ").append(getNamespace());
263      }
264      if (spaceProto.getRemove()) {
265        builder.append(", REMOVE => ").append(spaceProto.getRemove());
266      } else {
267        builder.append(", LIMIT => ").append(sizeToString(spaceProto.getSoftLimit()));
268        builder.append(", VIOLATION_POLICY => ").append(spaceProto.getViolationPolicy());
269      }
270      builder.append(" } ");
271    }
272    return builder.toString();
273  }
274
275  private Map<ThrottleType,TimedQuota> buildThrottleQuotas(Throttle proto) {
276    HashMap<ThrottleType,TimedQuota> quotas = new HashMap<>();
277    if (proto.hasReadNum()) {
278      quotas.put(ThrottleType.READ_NUMBER, proto.getReadNum());
279    }
280    if (proto.hasReadSize()) {
281      quotas.put(ThrottleType.READ_SIZE, proto.getReadSize());
282    }
283    if (proto.hasReqNum()) {
284      quotas.put(ThrottleType.REQUEST_NUMBER, proto.getReqNum());
285    }
286    if (proto.hasReqSize()) {
287      quotas.put(ThrottleType.REQUEST_SIZE, proto.getReqSize());
288    }
289    if (proto.hasWriteNum()) {
290      quotas.put(ThrottleType.WRITE_NUMBER, proto.getWriteNum());
291    }
292    if (proto.hasWriteSize()) {
293      quotas.put(ThrottleType.WRITE_SIZE, proto.getWriteSize());
294    }
295    return quotas;
296  }
297}