Skip to content

Commit da4fb51

Browse files
committed
Harden RocksDB retry detection and error accounting
Signed-off-by: Solanasol233 <solanasolisss@gmail.com>
1 parent 057dcb3 commit da4fb51

File tree

5 files changed

+195
-9
lines changed

5 files changed

+195
-9
lines changed

ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/storage/PathBasedWorldStateKeyValueStorage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ private void moveDBEntry(
386386
tx.commit();
387387
break;
388388
} catch (StorageException se) {
389-
if (!retried && se.getMessage().contains("RocksDBException: Busy")) {
389+
if (!retried && RocksDbStorageExceptionHelper.isBusyException(se)) {
390390
retried = true;
391391
} else {
392392
break;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright contributors to Hyperledger Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.trie.pathbased.common.storage;
16+
17+
import org.hyperledger.besu.plugin.services.exception.StorageException;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.Optional;
21+
22+
public final class RocksDbStorageExceptionHelper {
23+
24+
private static final String ROCKS_DB_EXCEPTION_CLASS_NAME = "org.rocksdb.RocksDBException";
25+
private static final String BUSY_STATUS_CODE_NAME = "Busy";
26+
27+
private RocksDbStorageExceptionHelper() {}
28+
29+
public static boolean isBusyException(final StorageException storageException) {
30+
return hasStatusCode(storageException, BUSY_STATUS_CODE_NAME);
31+
}
32+
33+
public static boolean hasStatusCode(
34+
final StorageException storageException, final String statusCodeName) {
35+
return getStatusCodeName(storageException).filter(statusCodeName::equals).isPresent();
36+
}
37+
38+
public static Optional<String> getStatusCodeName(final StorageException storageException) {
39+
return Optional.ofNullable(storageException.getCause())
40+
.filter(cause -> ROCKS_DB_EXCEPTION_CLASS_NAME.equals(cause.getClass().getName()))
41+
.flatMap(RocksDbStorageExceptionHelper::extractStatusCodeName);
42+
}
43+
44+
private static Optional<String> extractStatusCodeName(final Throwable rocksDbException) {
45+
try {
46+
// ethereum/core does not depend directly on rocksdbjni, so inspect the status via
47+
// reflection instead of relying on exception message text.
48+
final Method getStatusMethod = rocksDbException.getClass().getMethod("getStatus");
49+
final Object status = getStatusMethod.invoke(rocksDbException);
50+
if (status == null) {
51+
return Optional.empty();
52+
}
53+
54+
final Method getCodeMethod = status.getClass().getMethod("getCode");
55+
final Object statusCode = getCodeMethod.invoke(status);
56+
if (statusCode == null) {
57+
return Optional.empty();
58+
}
59+
60+
if (statusCode instanceof Enum<?> enumStatusCode) {
61+
return Optional.of(enumStatusCode.name());
62+
}
63+
return Optional.of(statusCode.toString());
64+
} catch (final ReflectiveOperationException ignored) {
65+
return Optional.empty();
66+
}
67+
}
68+
}

ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/StorageExceptionManager.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.EnumSet;
2020
import java.util.Optional;
21+
import java.util.concurrent.atomic.AtomicLong;
2122

2223
import org.rocksdb.RocksDBException;
2324
import org.rocksdb.Status;
@@ -29,7 +30,7 @@ public final class StorageExceptionManager {
2930

3031
private static final long ERROR_THRESHOLD = 1000;
3132

32-
private static long retryableErrorCounter;
33+
private static final AtomicLong retryableErrorCounter = new AtomicLong();
3334

3435
/**
3536
* Determines if an operation can be retried based on the error received. This method checks if
@@ -41,25 +42,25 @@ public final class StorageExceptionManager {
4142
* @return true if the operation can be retried, false otherwise
4243
*/
4344
public static boolean canRetryOnError(final StorageException e) {
44-
return Optional.of(e.getCause())
45+
return Optional.ofNullable(e.getCause())
4546
.filter(z -> z instanceof RocksDBException)
4647
.map(RocksDBException.class::cast)
4748
.map(RocksDBException::getStatus)
4849
.map(Status::getCode)
49-
.map(RETRYABLE_STATUS_CODES::contains)
50+
.filter(RETRYABLE_STATUS_CODES::contains)
5051
.map(
51-
result -> {
52-
retryableErrorCounter++;
53-
return result;
52+
statusCode -> {
53+
retryableErrorCounter.incrementAndGet();
54+
return true;
5455
})
5556
.orElse(false);
5657
}
5758

5859
public static long getRetryableErrorCounter() {
59-
return retryableErrorCounter;
60+
return retryableErrorCounter.get();
6061
}
6162

6263
public static boolean errorCountAtThreshold() {
63-
return retryableErrorCounter % ERROR_THRESHOLD == 1;
64+
return retryableErrorCounter.get() % ERROR_THRESHOLD == 1;
6465
}
6566
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright contributors to Hyperledger Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.eth.sync;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import org.hyperledger.besu.plugin.services.exception.StorageException;
20+
21+
import java.lang.reflect.Field;
22+
import java.util.concurrent.atomic.AtomicLong;
23+
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.rocksdb.RocksDBException;
27+
import org.rocksdb.Status;
28+
29+
public class StorageExceptionManagerTest {
30+
31+
@BeforeEach
32+
public void resetRetryableErrorCounter() throws ReflectiveOperationException {
33+
final Field retryableErrorCounterField =
34+
StorageExceptionManager.class.getDeclaredField("retryableErrorCounter");
35+
retryableErrorCounterField.setAccessible(true);
36+
((AtomicLong) retryableErrorCounterField.get(null)).set(0L);
37+
}
38+
39+
@Test
40+
public void shouldNotThrowWhenStorageExceptionCauseIsNull() {
41+
assertThat(StorageExceptionManager.canRetryOnError(new StorageException("missing cause")))
42+
.isFalse();
43+
assertThat(StorageExceptionManager.getRetryableErrorCounter()).isZero();
44+
}
45+
46+
@Test
47+
public void shouldNotCountNonRetryableRocksDbErrors() {
48+
final RocksDBException rocksDbException =
49+
new RocksDBException(new Status(Status.Code.Aborted, Status.SubCode.None, "aborted"));
50+
51+
assertThat(StorageExceptionManager.canRetryOnError(new StorageException(rocksDbException)))
52+
.isFalse();
53+
assertThat(StorageExceptionManager.getRetryableErrorCounter()).isZero();
54+
}
55+
56+
@Test
57+
public void shouldCountRetryableRocksDbErrors() {
58+
final RocksDBException rocksDbException =
59+
new RocksDBException(new Status(Status.Code.Busy, Status.SubCode.None, "busy"));
60+
61+
assertThat(StorageExceptionManager.canRetryOnError(new StorageException(rocksDbException)))
62+
.isTrue();
63+
assertThat(StorageExceptionManager.getRetryableErrorCounter()).isEqualTo(1L);
64+
assertThat(StorageExceptionManager.errorCountAtThreshold()).isTrue();
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright contributors to Hyperledger Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.trie.pathbased.common.storage;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import org.hyperledger.besu.plugin.services.exception.StorageException;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.rocksdb.RocksDBException;
23+
import org.rocksdb.Status;
24+
25+
public class RocksDbStorageExceptionHelperTest {
26+
27+
@Test
28+
public void shouldReturnBusyStatusCodeNameFromRocksDbException() {
29+
final StorageException storageException =
30+
new StorageException(
31+
new RocksDBException(new Status(Status.Code.Busy, Status.SubCode.None, "busy")));
32+
33+
assertThat(RocksDbStorageExceptionHelper.getStatusCodeName(storageException))
34+
.contains("Busy");
35+
assertThat(RocksDbStorageExceptionHelper.isBusyException(storageException)).isTrue();
36+
}
37+
38+
@Test
39+
public void shouldReturnEmptyWhenStorageExceptionHasNoCause() {
40+
assertThat(RocksDbStorageExceptionHelper.getStatusCodeName(new StorageException("missing")))
41+
.isEmpty();
42+
}
43+
44+
@Test
45+
public void shouldReturnEmptyWhenCauseIsNotRocksDbException() {
46+
assertThat(
47+
RocksDbStorageExceptionHelper.getStatusCodeName(
48+
new StorageException(new IllegalStateException("not rocksdb"))))
49+
.isEmpty();
50+
}
51+
}

0 commit comments

Comments
 (0)