|
| 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, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +package io.milvus.v2.utils; |
| 21 | + |
| 22 | +import io.grpc.StatusRuntimeException; |
| 23 | +import io.milvus.v2.client.RetryConfig; |
| 24 | +import io.milvus.v2.exception.ErrorCode; |
| 25 | +import io.milvus.v2.exception.MilvusClientException; |
| 26 | +import org.junit.jupiter.api.Assertions; |
| 27 | +import org.junit.jupiter.api.Test; |
| 28 | + |
| 29 | +import java.util.concurrent.atomic.AtomicInteger; |
| 30 | + |
| 31 | +public class RpcUtilsTest { |
| 32 | + |
| 33 | + @Test |
| 34 | + void testEarlyExitWhenPredictedBackoffExceedsMaxRetryTimeoutMs() { |
| 35 | + RpcUtils rpcUtils = new RpcUtils(); |
| 36 | + long maxRetryTimeoutMs = 5000; |
| 37 | + rpcUtils.retryConfig(RetryConfig.builder() |
| 38 | + .maxRetryTimes(10) |
| 39 | + .maxRetryTimeoutMs(maxRetryTimeoutMs) |
| 40 | + .initialBackOffMs(10) |
| 41 | + .maxBackOffMs(3000) |
| 42 | + .backOffMultiplier(3) |
| 43 | + .build()); |
| 44 | + |
| 45 | + long start = System.currentTimeMillis(); |
| 46 | + |
| 47 | + MilvusClientException thrown = Assertions.assertThrows(MilvusClientException.class, () -> { |
| 48 | + rpcUtils.retry(() -> { |
| 49 | + throw new StatusRuntimeException( |
| 50 | + io.grpc.Status.UNAVAILABLE.withDescription("server unavailable")); |
| 51 | + }); |
| 52 | + }); |
| 53 | + |
| 54 | + long elapsed = System.currentTimeMillis() - start; |
| 55 | + |
| 56 | + Assertions.assertEquals(ErrorCode.TIMEOUT, thrown.getErrorCode(), |
| 57 | + "Should fail with TIMEOUT error code"); |
| 58 | + // Backoff sequence (initial=10ms, multiplier=3, max=3000ms): |
| 59 | + // k=1 @~0ms → sleep 10ms |
| 60 | + // k=2 @~10ms → sleep 30ms |
| 61 | + // k=3 @~40ms → sleep 90ms |
| 62 | + // k=4 @~130ms → sleep 270ms |
| 63 | + // k=5 @~400ms → sleep 810ms |
| 64 | + // k=6 @~1210ms → sleep 2430ms |
| 65 | + // k=7 @~3640ms → next backoff 3000ms, 3640+3000=6640 > 5000ms → TIMEOUT |
| 66 | + Assertions.assertTrue(elapsed <= 4000, |
| 67 | + "Retry should respect maxRetryTimeoutMs(5000ms), but took " + elapsed + "ms"); |
| 68 | + } |
| 69 | + |
| 70 | + @Test |
| 71 | + void testMaxRetryTimes() { |
| 72 | + RpcUtils rpcUtils = new RpcUtils(); |
| 73 | + int maxRetryTimes = 3; |
| 74 | + rpcUtils.retryConfig(RetryConfig.builder() |
| 75 | + .maxRetryTimes(maxRetryTimes) |
| 76 | + .maxRetryTimeoutMs(60000) // large timeout so retry times is the limiting factor |
| 77 | + .initialBackOffMs(10) |
| 78 | + .maxBackOffMs(100) |
| 79 | + .backOffMultiplier(2) |
| 80 | + .build()); |
| 81 | + |
| 82 | + AtomicInteger callCount = new AtomicInteger(0); |
| 83 | + |
| 84 | + MilvusClientException thrown = Assertions.assertThrows(MilvusClientException.class, () -> { |
| 85 | + rpcUtils.retry(() -> { |
| 86 | + callCount.incrementAndGet(); |
| 87 | + throw new StatusRuntimeException( |
| 88 | + io.grpc.Status.UNAVAILABLE.withDescription("server unavailable")); |
| 89 | + }); |
| 90 | + }); |
| 91 | + |
| 92 | + Assertions.assertEquals(ErrorCode.TIMEOUT, thrown.getErrorCode(), |
| 93 | + "Should fail with TIMEOUT error code"); |
| 94 | + Assertions.assertEquals(maxRetryTimes, callCount.get(), |
| 95 | + "Should have retried exactly maxRetryTimes(" + maxRetryTimes + ") times, but got " + callCount.get()); |
| 96 | + } |
| 97 | + |
| 98 | + @Test |
| 99 | + void testTimeoutAfterSlowCallExceedsMaxRetryTimeoutMs() { |
| 100 | + RpcUtils rpcUtils = new RpcUtils(); |
| 101 | + long maxRetryTimeoutMs = 2000; |
| 102 | + rpcUtils.retryConfig(RetryConfig.builder() |
| 103 | + .maxRetryTimes(10) |
| 104 | + .maxRetryTimeoutMs(maxRetryTimeoutMs) |
| 105 | + .initialBackOffMs(50) |
| 106 | + .maxBackOffMs(500) |
| 107 | + .backOffMultiplier(2) |
| 108 | + .build()); |
| 109 | + |
| 110 | + AtomicInteger callCount = new AtomicInteger(0); |
| 111 | + |
| 112 | + long start = System.currentTimeMillis(); |
| 113 | + |
| 114 | + MilvusClientException thrown = Assertions.assertThrows(MilvusClientException.class, () -> { |
| 115 | + rpcUtils.retry(() -> { |
| 116 | + callCount.incrementAndGet(); |
| 117 | + // Simulate a slow RPC call that takes 500ms each time |
| 118 | + Thread.sleep(500); |
| 119 | + throw new StatusRuntimeException( |
| 120 | + io.grpc.Status.UNAVAILABLE.withDescription("server unavailable")); |
| 121 | + }); |
| 122 | + }); |
| 123 | + |
| 124 | + long elapsed = System.currentTimeMillis() - start; |
| 125 | + |
| 126 | + Assertions.assertEquals(ErrorCode.TIMEOUT, thrown.getErrorCode(), |
| 127 | + "Should fail with TIMEOUT error code when slow calls accumulate beyond maxRetryTimeoutMs"); |
| 128 | + // Timeline (slow call sleep=500ms, backoff: initial=50ms, multiplier=2, max=500ms): |
| 129 | + // k=1 @~0ms → sleep 500ms, call ends @500ms |
| 130 | + // backoff 50ms, total ~550ms < 2000ms → continue |
| 131 | + // k=2 @~550ms → sleep 500ms, call ends @~1050ms |
| 132 | + // backoff 100ms, total ~1150ms < 2000ms → continue |
| 133 | + // k=3 @~1150ms → sleep 500ms, call ends @~1650ms |
| 134 | + // backoff 200ms, total ~1850ms < 2000ms → continue |
| 135 | + // k=4 @~1850ms → sleep 500ms, call ends @~2350ms |
| 136 | + // elapsed(2350ms) >= maxRetryTimeoutMs(2000ms) |
| 137 | + Assertions.assertEquals(4, callCount.get(), "Should have 4 times, but got " + callCount.get()); |
| 138 | + Assertions.assertTrue(elapsed > maxRetryTimeoutMs, |
| 139 | + "Elapsed time should greater than maxRetryTimeoutMs, but was " + elapsed + "ms"); |
| 140 | + Assertions.assertTrue(elapsed < maxRetryTimeoutMs + 1000, |
| 141 | + "Should not exceed maxRetryTimeoutMs by too much, elapsed was " + elapsed + "ms"); |
| 142 | + } |
| 143 | +} |
0 commit comments