Skip to content

Commit 66bb0fa

Browse files
committed
HBASE-30234: Replication shippedBytes metric overflows to negative due to int truncation of batch size
Several size-tracking variables in the replication source pipeline used int instead of long, so once a WAL entry batch (or a bulk load's referenced store files) exceeded Integer.MAX_VALUE (~2GB) the value silently overflowed. This surfaced as a negative shippedBytes JMX metric and as broken throttling, since the truncated (often negative) size was handed to the bandwidth throttler. Widen the size to long end-to-end: - ReplicationSourceShipper#shipEdits keeps getHeapSize() as long. - ReplicationSourceInterface/ReplicationSource#tryThrottle and ReplicationThrottler#getNextSleepInterval accept a long size. - MetricsSource#shipBatch takes a long sizeInBytes (sink counters are long). - ReplicationEndpoint.ReplicateContext#size and its accessors are long. - ReplicationSourceWALReader#sizeOfStoreFilesIncludeBulkLoad accumulates and returns long instead of casting each addition to int. Add TestReplicationSizeOverflow (store-file-size accumulation, shippedBytes metric path, ReplicateContext) and a >2GB case in TestReplicationThrottler.
1 parent f9a41c7 commit 66bb0fa

10 files changed

Lines changed: 169 additions & 14 deletions

File tree

hbase-server/src/main/java/org/apache/hadoop/hbase/replication/ReplicationEndpoint.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public Abortable getAbortable() {
158158
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.REPLICATION)
159159
static class ReplicateContext {
160160
List<Entry> entries;
161-
int size;
161+
long size;
162162
String walGroupId;
163163
int timeout;
164164

@@ -171,7 +171,7 @@ public ReplicateContext setEntries(List<Entry> entries) {
171171
return this;
172172
}
173173

174-
public ReplicateContext setSize(int size) {
174+
public ReplicateContext setSize(long size) {
175175
this.size = size;
176176
return this;
177177
}
@@ -185,7 +185,7 @@ public List<Entry> getEntries() {
185185
return entries;
186186
}
187187

188-
public int getSize() {
188+
public long getSize() {
189189
return size;
190190
}
191191

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public void incrLogEditsFiltered() {
210210
* Convience method to apply changes to metrics do to shipping a batch of logs.
211211
* @param batchSize the size of the batch that was shipped to sinks.
212212
*/
213-
public void shipBatch(long batchSize, int sizeInBytes) {
213+
public void shipBatch(long batchSize, long sizeInBytes) {
214214
singleSourceSource.incrBatchesShipped(1);
215215
globalSourceSource.incrBatchesShipped(1);
216216

@@ -258,7 +258,7 @@ public long getOpsShipped() {
258258
* @param batchSize the size of the batch that was shipped to sinks.
259259
* @param hfiles total number of hfiles shipped to sinks.
260260
*/
261-
public void shipBatch(long batchSize, int sizeInBytes, long hfiles) {
261+
public void shipBatch(long batchSize, long sizeInBytes, long hfiles) {
262262
shipBatch(batchSize, sizeInBytes);
263263
singleSourceSource.incrHFilesShipped(hfiles);
264264
globalSourceSource.incrHFilesShipped(hfiles);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ public ReplicationSourceManager getSourceManager() {
495495
}
496496

497497
@Override
498-
public void tryThrottle(int batchSize) throws InterruptedException {
498+
public void tryThrottle(long batchSize) throws InterruptedException {
499499
checkBandwidthChangeAndResetThrottler();
500500
if (throttler.isEnabled()) {
501501
long sleepTicks = throttler.getNextSleepInterval(batchSize);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ default boolean isSyncReplication() {
167167
* Try to throttle when the peer config with a bandwidth
168168
* @param batchSize entries size will be pushed
169169
*/
170-
void tryThrottle(int batchSize) throws InterruptedException;
170+
void tryThrottle(long batchSize) throws InterruptedException;
171171

172172
/**
173173
* Call this after the shipper thread ship some entries to peer cluster.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ protected void postFinish() {
204204
void shipEdits(WALEntryBatch entryBatch) throws IOException {
205205
List<Entry> entries = entryBatch.getWalEntries();
206206
int sleepMultiplier = 0;
207-
int currentSize = (int) entryBatch.getHeapSize();
207+
long currentSize = entryBatch.getHeapSize();
208208
MetricsSource metrics = source.getSourceMetrics();
209209
if (metrics != null && !entries.isEmpty()) {
210210
metrics.setTimeStampNextToReplicate(entries.get(entries.size() - 1).getKey().getWriteTime());

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -362,14 +362,17 @@ private Pair<Integer, Integer> countDistinctRowKeysAndHFiles(WALEdit edit) {
362362
return result;
363363
}
364364

365+
// Package-private static (a pure function of the edit) so it can be unit tested directly; see
366+
// TestReplicationSizeOverflow. Uses long throughout to avoid overflow when the bulk load store
367+
// files sum exceeds Integer.MAX_VALUE (~2GB).
365368
/**
366369
* Calculate the total size of all the store files
367370
* @param edit edit to count row keys from
368371
* @return the total size of the store files
369372
*/
370-
private int sizeOfStoreFilesIncludeBulkLoad(WALEdit edit) {
373+
static long sizeOfStoreFilesIncludeBulkLoad(WALEdit edit) {
371374
List<Cell> cells = edit.getCells();
372-
int totalStoreFilesSize = 0;
375+
long totalStoreFilesSize = 0;
373376

374377
int totalCells = edit.size();
375378
for (int i = 0; i < totalCells; i++) {
@@ -379,8 +382,7 @@ private int sizeOfStoreFilesIncludeBulkLoad(WALEdit edit) {
379382
List<StoreDescriptor> stores = bld.getStoresList();
380383
int totalStores = stores.size();
381384
for (int j = 0; j < totalStores; j++) {
382-
totalStoreFilesSize =
383-
(int) (totalStoreFilesSize + stores.get(j).getStoreFileSizeBytes());
385+
totalStoreFilesSize += stores.get(j).getStoreFileSizeBytes();
384386
}
385387
} catch (IOException e) {
386388
LOG.error("Failed to deserialize bulk load entry from wal edit. "

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public boolean isEnabled() {
5959
* @param size is the size of edits to be pushed
6060
* @return sleep interval for throttling control
6161
*/
62-
public long getNextSleepInterval(final int size) {
62+
public long getNextSleepInterval(final long size) {
6363
if (!this.enabled) {
6464
return 0;
6565
}

hbase-server/src/test/java/org/apache/hadoop/hbase/replication/ReplicationSourceDummy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public ReplicationSourceManager getSourceManager() {
146146
}
147147

148148
@Override
149-
public void tryThrottle(int batchSize) throws InterruptedException {
149+
public void tryThrottle(long batchSize) throws InterruptedException {
150150
}
151151

152152
@Override
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.replication.regionserver;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.verify;
24+
25+
import java.util.Collections;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import org.apache.hadoop.fs.Path;
30+
import org.apache.hadoop.hbase.TableName;
31+
import org.apache.hadoop.hbase.client.RegionInfo;
32+
import org.apache.hadoop.hbase.client.RegionInfoBuilder;
33+
import org.apache.hadoop.hbase.replication.ReplicationEndpoint;
34+
import org.apache.hadoop.hbase.testclassification.ReplicationTests;
35+
import org.apache.hadoop.hbase.testclassification.SmallTests;
36+
import org.apache.hadoop.hbase.util.Bytes;
37+
import org.apache.hadoop.hbase.wal.WALEdit;
38+
import org.junit.jupiter.api.Tag;
39+
import org.junit.jupiter.api.Test;
40+
41+
import org.apache.hbase.thirdparty.com.google.protobuf.UnsafeByteOperations;
42+
43+
import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
44+
import org.apache.hadoop.hbase.shaded.protobuf.generated.WALProtos.BulkLoadDescriptor;
45+
46+
/**
47+
* Unit tests for HBASE-30234: several size-tracking variables in the replication source pipeline
48+
* used {@code int} instead of {@code long}, causing integer overflow (and negative metrics /
49+
* broken throttling) once a batch exceeds {@link Integer#MAX_VALUE} (~2GB). These tests exercise
50+
* each fixed site with a value that overflows {@code int}.
51+
*/
52+
@Tag(ReplicationTests.TAG)
53+
@Tag(SmallTests.TAG)
54+
public class TestReplicationSizeOverflow {
55+
56+
private static final RegionInfo RI =
57+
RegionInfoBuilder.newBuilder(TableName.valueOf("testReplicationSizeOverflow")).build();
58+
59+
/** A byte count that is larger than Integer.MAX_VALUE and would wrap negative as an int. */
60+
private static final long OVER_2GB = 3_500_000_000L;
61+
62+
/**
63+
* The sum of the bulk load store file sizes must not overflow when it exceeds ~2GB.
64+
* {@link ReplicationSourceWALReader#sizeOfStoreFilesIncludeBulkLoad} previously accumulated the
65+
* sizes into an int and cast on every step, wrapping the total to a negative value.
66+
*/
67+
@Test
68+
public void testSizeOfStoreFilesIncludeBulkLoadDoesNotOverflow() {
69+
long size1 = 2_000_000_000L;
70+
long size2 = 1_500_000_000L;
71+
long expected = size1 + size2;
72+
// sanity: the total genuinely overflows a signed int
73+
assertTrue(expected > Integer.MAX_VALUE);
74+
75+
Map<byte[], List<Path>> storeFiles = new HashMap<>();
76+
Map<String, Long> storeFilesSize = new HashMap<>();
77+
Path hfile1 = new Path("f1");
78+
storeFiles.put(Bytes.toBytes("f1"), Collections.singletonList(hfile1));
79+
storeFilesSize.put(hfile1.getName(), size1);
80+
Path hfile2 = new Path("f2");
81+
storeFiles.put(Bytes.toBytes("f2"), Collections.singletonList(hfile2));
82+
storeFilesSize.put(hfile2.getName(), size2);
83+
84+
BulkLoadDescriptor desc = ProtobufUtil.toBulkLoadDescriptor(RI.getTable(),
85+
UnsafeByteOperations.unsafeWrap(RI.getEncodedNameAsBytes()), storeFiles, storeFilesSize, 1);
86+
WALEdit edit = WALEdit.createBulkLoadEvent(RI, desc);
87+
88+
assertEquals(expected, ReplicationSourceWALReader.sizeOfStoreFilesIncludeBulkLoad(edit));
89+
}
90+
91+
/**
92+
* The shipped-bytes metric must receive the full batch size. Previously
93+
* {@link MetricsSource#shipBatch} took an int {@code sizeInBytes}, so a &gt;2GB batch was
94+
* incremented into the counter as a negative value.
95+
*/
96+
@Test
97+
public void testShipBatchDoesNotTruncateShippedBytes() {
98+
MetricsReplicationSourceSource single = mock(MetricsReplicationSourceSource.class);
99+
MetricsReplicationGlobalSourceSource global =
100+
mock(MetricsReplicationGlobalSourceSource.class);
101+
MetricsSource metrics = new MetricsSource("test-source", single, global, new HashMap<>());
102+
103+
metrics.shipBatch(10L, OVER_2GB, 2L);
104+
105+
// the full long size must reach both counters unchanged, not a truncated int
106+
verify(single).incrShippedBytes(OVER_2GB);
107+
verify(global).incrShippedBytes(OVER_2GB);
108+
verify(single).incrHFilesShipped(2L);
109+
verify(global).incrHFilesShipped(2L);
110+
}
111+
112+
/**
113+
* The replicate context must carry the full batch size to the endpoint. Previously the
114+
* {@code size} field and its accessors were int, truncating a &gt;2GB batch.
115+
*/
116+
@Test
117+
public void testReplicateContextSizeDoesNotOverflow() {
118+
ReplicationEndpoint.ReplicateContext ctx = new ReplicationEndpoint.ReplicateContext();
119+
ctx.setSize(OVER_2GB);
120+
assertEquals(OVER_2GB, ctx.getSize());
121+
}
122+
}

hbase-server/src/test/java/org/apache/hadoop/hbase/replication/regionserver/TestReplicationThrottler.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import org.apache.hadoop.hbase.testclassification.ReplicationTests;
2424
import org.apache.hadoop.hbase.testclassification.SmallTests;
25+
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
26+
import org.apache.hadoop.hbase.util.ManualEnvironmentEdge;
2527
import org.junit.jupiter.api.Tag;
2628
import org.junit.jupiter.api.Test;
2729
import org.slf4j.Logger;
@@ -113,4 +115,33 @@ public void testThrottling() {
113115
assertTrue(ticks1 >= 375 && ticks1 <= 500);
114116
}
115117
}
118+
119+
/**
120+
* HBASE-30234: a batch whose size exceeds Integer.MAX_VALUE (~2GB) must be treated as a large
121+
* positive size instead of being silently truncated to a negative int. With truncation, the
122+
* "delay to next cycle" branch would compare a negative sum against the bandwidth and wrongly
123+
* return 0 (no throttling); with the fix it correctly delays the push to the next cycle.
124+
*/
125+
@Test
126+
public void testLargeSizeDoesNotOverflow() {
127+
LOG.info("testLargeSizeDoesNotOverflow");
128+
// Freeze the clock so the assertion is deterministic (no cycle boundary is crossed).
129+
ManualEnvironmentEdge edge = new ManualEnvironmentEdge();
130+
edge.setValue(1000);
131+
EnvironmentEdgeManager.injectEdge(edge);
132+
try {
133+
// bandwidth of 1 byte/cycle so any positive push exceeds it
134+
ReplicationThrottler throttler = new ReplicationThrottler(1);
135+
// prime the current cycle so cyclePushSize > 0 (enables the "delay to next cycle" branch)
136+
throttler.addPushSize(1);
137+
// a size larger than Integer.MAX_VALUE; as an int this would wrap to a negative value
138+
long hugeSize = (long) Integer.MAX_VALUE + 1L;
139+
long ticks = throttler.getNextSleepInterval(hugeSize);
140+
// delayed to next cycle: cycleStartTick(1000) + one cycle(100) - now(1000) == 100ms.
141+
// A truncated (negative) size would fail the bandwidth check and wrongly return 0.
142+
assertEquals(100, ticks);
143+
} finally {
144+
EnvironmentEdgeManager.reset();
145+
}
146+
}
116147
}

0 commit comments

Comments
 (0)