Skip to content

Commit 98eb3e0

Browse files
authored
HBASE-28349 Count atomic operations against read quotas (#5668)
Signed-off-by: Bryan Beaudreault <[email protected]>
1 parent e85557a commit 98eb3e0

5 files changed

Lines changed: 283 additions & 8 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/OperationQuota.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public interface OperationQuota {
3232
public enum OperationType {
3333
MUTATE,
3434
GET,
35-
SCAN
35+
SCAN,
36+
CHECK_AND_MUTATE
3637
}
3738

3839
/**

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/QuotaUtil.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.slf4j.LoggerFactory;
5252

5353
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
54+
import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos;
5455
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.TimeUnit;
5556
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos;
5657
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.QuotaScope;
@@ -177,6 +178,31 @@ public static void deleteRegionServerQuota(final Connection connection, final St
177178
deleteQuotas(connection, getRegionServerRowKey(regionServer));
178179
}
179180

181+
public static OperationQuota.OperationType getQuotaOperationType(ClientProtos.Action action,
182+
boolean hasCondition) {
183+
if (action.hasMutation()) {
184+
return getQuotaOperationType(action.getMutation(), hasCondition);
185+
}
186+
return OperationQuota.OperationType.GET;
187+
}
188+
189+
public static OperationQuota.OperationType
190+
getQuotaOperationType(ClientProtos.MutateRequest mutateRequest) {
191+
return getQuotaOperationType(mutateRequest.getMutation(), mutateRequest.hasCondition());
192+
}
193+
194+
private static OperationQuota.OperationType
195+
getQuotaOperationType(ClientProtos.MutationProto mutationProto, boolean hasCondition) {
196+
ClientProtos.MutationProto.MutationType mutationType = mutationProto.getMutateType();
197+
if (
198+
hasCondition || mutationType == ClientProtos.MutationProto.MutationType.APPEND
199+
|| mutationType == ClientProtos.MutationProto.MutationType.INCREMENT
200+
) {
201+
return OperationQuota.OperationType.CHECK_AND_MUTATE;
202+
}
203+
return OperationQuota.OperationType.MUTATE;
204+
}
205+
180206
protected static void switchExceedThrottleQuota(final Connection connection,
181207
boolean exceedThrottleQuotaEnabled) throws IOException {
182208
if (exceedThrottleQuotaEnabled) {

hbase-server/src/main/java/org/apache/hadoop/hbase/quotas/RegionServerRpcQuotaManager.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,25 +171,33 @@ public OperationQuota checkQuota(final Region region, final OperationQuota.Opera
171171
return checkQuota(region, 0, 1, 0);
172172
case MUTATE:
173173
return checkQuota(region, 1, 0, 0);
174+
case CHECK_AND_MUTATE:
175+
return checkQuota(region, 1, 1, 0);
174176
}
175177
throw new RuntimeException("Invalid operation type: " + type);
176178
}
177179

178180
/**
179181
* Check the quota for the current (rpc-context) user. Returns the OperationQuota used to get the
180182
* available quota and to report the data/usage of the operation.
181-
* @param region the region where the operation will be performed
182-
* @param actions the "multi" actions to perform
183+
* @param region the region where the operation will be performed
184+
* @param actions the "multi" actions to perform
185+
* @param hasCondition whether the RegionAction has a condition
183186
* @return the OperationQuota
184187
* @throws RpcThrottlingException if the operation cannot be executed due to quota exceeded.
185188
*/
186-
public OperationQuota checkQuota(final Region region, final List<ClientProtos.Action> actions)
187-
throws IOException, RpcThrottlingException {
189+
public OperationQuota checkQuota(final Region region, final List<ClientProtos.Action> actions,
190+
boolean hasCondition) throws IOException, RpcThrottlingException {
188191
int numWrites = 0;
189192
int numReads = 0;
190193
for (final ClientProtos.Action action : actions) {
191194
if (action.hasMutation()) {
192195
numWrites++;
196+
OperationQuota.OperationType operationType =
197+
QuotaUtil.getQuotaOperationType(action, hasCondition);
198+
if (operationType == OperationQuota.OperationType.CHECK_AND_MUTATE) {
199+
numReads++;
200+
}
193201
} else if (action.hasGet()) {
194202
numReads++;
195203
}

hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2679,7 +2679,8 @@ public MultiResponse multi(final RpcController rpcc, final MultiRequest request)
26792679

26802680
try {
26812681
region = getRegion(regionSpecifier);
2682-
quota = getRpcQuotaManager().checkQuota(region, regionAction.getActionList());
2682+
quota = getRpcQuotaManager().checkQuota(region, regionAction.getActionList(),
2683+
regionAction.hasCondition());
26832684
} catch (IOException e) {
26842685
failRegionAction(responseBuilder, regionActionResultBuilder, regionAction, cellScanner, e);
26852686
return responseBuilder.build();
@@ -2741,7 +2742,8 @@ public MultiResponse multi(final RpcController rpcc, final MultiRequest request)
27412742

27422743
try {
27432744
region = getRegion(regionSpecifier);
2744-
quota = getRpcQuotaManager().checkQuota(region, regionAction.getActionList());
2745+
quota = getRpcQuotaManager().checkQuota(region, regionAction.getActionList(),
2746+
regionAction.hasCondition());
27452747
} catch (IOException e) {
27462748
failRegionAction(responseBuilder, regionActionResultBuilder, regionAction, cellScanner, e);
27472749
continue; // For this region it's a failure.
@@ -2924,7 +2926,8 @@ public MutateResponse mutate(final RpcController rpcc, final MutateRequest reque
29242926
server.getMemStoreFlusher().reclaimMemStoreMemory();
29252927
}
29262928
long nonceGroup = request.hasNonceGroup() ? request.getNonceGroup() : HConstants.NO_NONCE;
2927-
quota = getRpcQuotaManager().checkQuota(region, OperationQuota.OperationType.MUTATE);
2929+
OperationQuota.OperationType operationType = QuotaUtil.getQuotaOperationType(request);
2930+
quota = getRpcQuotaManager().checkQuota(region, operationType);
29282931
ActivePolicyEnforcement spaceQuotaEnforcement =
29292932
getSpaceQuotaManager().getActiveEnforcements();
29302933

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.quotas;
19+
20+
import java.io.IOException;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.UUID;
24+
import java.util.concurrent.TimeUnit;
25+
import org.apache.hadoop.hbase.HBaseClassTestRule;
26+
import org.apache.hadoop.hbase.HBaseTestingUtil;
27+
import org.apache.hadoop.hbase.HConstants;
28+
import org.apache.hadoop.hbase.TableName;
29+
import org.apache.hadoop.hbase.client.Admin;
30+
import org.apache.hadoop.hbase.client.CheckAndMutate;
31+
import org.apache.hadoop.hbase.client.Increment;
32+
import org.apache.hadoop.hbase.client.Put;
33+
import org.apache.hadoop.hbase.client.RowMutations;
34+
import org.apache.hadoop.hbase.client.Table;
35+
import org.apache.hadoop.hbase.security.User;
36+
import org.apache.hadoop.hbase.testclassification.MediumTests;
37+
import org.apache.hadoop.hbase.testclassification.RegionServerTests;
38+
import org.apache.hadoop.hbase.util.Bytes;
39+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
40+
import org.junit.AfterClass;
41+
import org.junit.BeforeClass;
42+
import org.junit.ClassRule;
43+
import org.junit.Test;
44+
import org.junit.experimental.categories.Category;
45+
import org.slf4j.Logger;
46+
import org.slf4j.LoggerFactory;
47+
48+
@Category({ RegionServerTests.class, MediumTests.class })
49+
public class TestAtomicReadQuota {
50+
@ClassRule
51+
public static final HBaseClassTestRule CLASS_RULE =
52+
HBaseClassTestRule.forClass(TestAtomicReadQuota.class);
53+
private static final Logger LOG = LoggerFactory.getLogger(TestAtomicReadQuota.class);
54+
private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
55+
private static final TableName TABLE_NAME = TableName.valueOf(UUID.randomUUID().toString());
56+
private static final byte[] FAMILY = Bytes.toBytes("cf");
57+
private static final byte[] QUALIFIER = Bytes.toBytes("q");
58+
59+
@AfterClass
60+
public static void tearDown() throws Exception {
61+
ThrottleQuotaTestUtil.clearQuotaCache(TEST_UTIL);
62+
EnvironmentEdgeManager.reset();
63+
TEST_UTIL.deleteTable(TABLE_NAME);
64+
TEST_UTIL.shutdownMiniCluster();
65+
}
66+
67+
@BeforeClass
68+
public static void setUpBeforeClass() throws Exception {
69+
TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true);
70+
TEST_UTIL.getConfiguration().setInt(QuotaCache.REFRESH_CONF_KEY, 1000);
71+
TEST_UTIL.startMiniCluster(1);
72+
TEST_UTIL.waitTableAvailable(QuotaTableUtil.QUOTA_TABLE_NAME);
73+
TEST_UTIL.createTable(TABLE_NAME, FAMILY);
74+
TEST_UTIL.waitTableAvailable(TABLE_NAME);
75+
QuotaCache.TEST_FORCE_REFRESH = true;
76+
}
77+
78+
@Test
79+
public void testIncrementCountedAgainstReadCapacity() throws Exception {
80+
setupQuota();
81+
82+
Increment inc = new Increment(Bytes.toBytes(UUID.randomUUID().toString()));
83+
inc.addColumn(FAMILY, QUALIFIER, 1);
84+
testThrottle(table -> table.increment(inc));
85+
}
86+
87+
@Test
88+
public void testConditionalRowMutationsCountedAgainstReadCapacity() throws Exception {
89+
setupQuota();
90+
91+
byte[] row = Bytes.toBytes(UUID.randomUUID().toString());
92+
Increment inc = new Increment(row);
93+
inc.addColumn(FAMILY, Bytes.toBytes("doot"), 1);
94+
Put put = new Put(row);
95+
put.addColumn(FAMILY, Bytes.toBytes("doot"), Bytes.toBytes("v"));
96+
97+
RowMutations rowMutations = new RowMutations(row);
98+
rowMutations.add(inc);
99+
rowMutations.add(put);
100+
testThrottle(table -> table.mutateRow(rowMutations));
101+
}
102+
103+
@Test
104+
public void testNonConditionalRowMutationsOmittedFromReadCapacity() throws Exception {
105+
setupQuota();
106+
107+
byte[] row = Bytes.toBytes(UUID.randomUUID().toString());
108+
Put put = new Put(row);
109+
put.addColumn(FAMILY, Bytes.toBytes("doot"), Bytes.toBytes("v"));
110+
111+
RowMutations rowMutations = new RowMutations(row);
112+
rowMutations.add(put);
113+
try (Table table = getTable()) {
114+
for (int i = 0; i < 100; i++) {
115+
table.mutateRow(rowMutations);
116+
}
117+
}
118+
}
119+
120+
@Test
121+
public void testNonAtomicPutOmittedFromReadCapacity() throws Exception {
122+
setupQuota();
123+
124+
byte[] row = Bytes.toBytes(UUID.randomUUID().toString());
125+
Put put = new Put(row);
126+
put.addColumn(FAMILY, Bytes.toBytes("doot"), Bytes.toBytes("v"));
127+
try (Table table = getTable()) {
128+
for (int i = 0; i < 100; i++) {
129+
table.put(put);
130+
}
131+
}
132+
}
133+
134+
@Test
135+
public void testNonAtomicMultiPutOmittedFromReadCapacity() throws Exception {
136+
setupQuota();
137+
138+
Put put1 = new Put(Bytes.toBytes(UUID.randomUUID().toString()));
139+
put1.addColumn(FAMILY, Bytes.toBytes("doot"), Bytes.toBytes("v"));
140+
Put put2 = new Put(Bytes.toBytes(UUID.randomUUID().toString()));
141+
put2.addColumn(FAMILY, Bytes.toBytes("doot"), Bytes.toBytes("v"));
142+
143+
Increment inc = new Increment(Bytes.toBytes(UUID.randomUUID().toString()));
144+
inc.addColumn(FAMILY, Bytes.toBytes("doot"), 1);
145+
146+
List<Put> puts = new ArrayList<>(2);
147+
puts.add(put1);
148+
puts.add(put2);
149+
150+
try (Table table = getTable()) {
151+
for (int i = 0; i < 100; i++) {
152+
table.put(puts);
153+
}
154+
}
155+
}
156+
157+
@Test
158+
public void testCheckAndMutateCountedAgainstReadCapacity() throws Exception {
159+
setupQuota();
160+
161+
byte[] row = Bytes.toBytes(UUID.randomUUID().toString());
162+
byte[] value = Bytes.toBytes("v");
163+
Put put = new Put(row);
164+
put.addColumn(FAMILY, Bytes.toBytes("doot"), value);
165+
CheckAndMutate checkAndMutate =
166+
CheckAndMutate.newBuilder(row).ifEquals(FAMILY, QUALIFIER, value).build(put);
167+
168+
testThrottle(table -> table.checkAndMutate(checkAndMutate));
169+
}
170+
171+
@Test
172+
public void testAtomicBatchCountedAgainstReadCapacity() throws Exception {
173+
setupQuota();
174+
175+
byte[] row = Bytes.toBytes(UUID.randomUUID().toString());
176+
Increment inc = new Increment(row);
177+
inc.addColumn(FAMILY, Bytes.toBytes("doot"), 1);
178+
179+
List<Increment> incs = new ArrayList<>(2);
180+
incs.add(inc);
181+
incs.add(inc);
182+
183+
testThrottle(table -> {
184+
Object[] results = new Object[] {};
185+
table.batch(incs, results);
186+
return results;
187+
});
188+
}
189+
190+
private void setupQuota() throws Exception {
191+
try (Admin admin = TEST_UTIL.getAdmin()) {
192+
admin.setQuota(QuotaSettingsFactory.throttleUser(User.getCurrent().getShortName(),
193+
ThrottleType.READ_NUMBER, 1, TimeUnit.MINUTES));
194+
}
195+
ThrottleQuotaTestUtil.triggerUserCacheRefresh(TEST_UTIL, false, TABLE_NAME);
196+
}
197+
198+
private void cleanupQuota() throws Exception {
199+
try (Admin admin = TEST_UTIL.getAdmin()) {
200+
admin.setQuota(QuotaSettingsFactory.unthrottleUser(User.getCurrent().getShortName()));
201+
}
202+
ThrottleQuotaTestUtil.triggerUserCacheRefresh(TEST_UTIL, true, TABLE_NAME);
203+
}
204+
205+
private void testThrottle(ThrowingFunction<Table, ?> request) throws Exception {
206+
try (Table table = getTable()) {
207+
// we have a read quota configured, so this should fail
208+
TEST_UTIL.waitFor(60_000, () -> {
209+
try {
210+
request.run(table);
211+
return false;
212+
} catch (Exception e) {
213+
boolean success = e.getCause() instanceof RpcThrottlingException;
214+
if (!success) {
215+
LOG.error("Unexpected exception", e);
216+
}
217+
return success;
218+
}
219+
});
220+
} finally {
221+
cleanupQuota();
222+
}
223+
}
224+
225+
private Table getTable() throws IOException {
226+
TEST_UTIL.getConfiguration().setInt("hbase.client.pause", 100);
227+
TEST_UTIL.getConfiguration().setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 1);
228+
return TEST_UTIL.getConnection().getTableBuilder(TABLE_NAME, null).setOperationTimeout(250)
229+
.build();
230+
}
231+
232+
@FunctionalInterface
233+
private interface ThrowingFunction<I, O> {
234+
O run(I input) throws Exception;
235+
}
236+
237+
}

0 commit comments

Comments
 (0)