Skip to content

Commit 73b6246

Browse files
LEGLINK-186: Fail fast on resource-cache blob calls so the retry-topic ladder owns retrying
The Azure SDK's default retry policy (4 exponential tries, effectively unbounded per-try timeout) hid ~3 minutes of silent retrying inside every consumer attempt during an ABS outage - blocking the single-threaded consumer executor (head-of-line blocking for all records) and multiplying the retry-topic ladder, which stretched from its designed ~3.5 minutes to ~11 before dead-lettering. The cache blob client now uses a fail-fast policy: FIXED, maxTries=2 (initial call + one quick retry to absorb a dropped connection), tryTimeout=10s, 1s delay. Real retrying belongs to the Kafka retry-topic ladder, which is visible (log lines, attempt headers), configurable, and ends in a dead letter. Both knobs are configuration properties (resource-cache.blob-storage.max-tries / .try-timeout-seconds). Measured live with azurite stopped: attempt latency dropped from ~2m48s to 14.9s, and the recovery leg (azurite restored before the redelivery) completed the empty-cache success path unchanged. Deliberately not applied to BlobStorageService (report payload uploads): that client is on the success path, where the SDK's patient retries help rather than harm. Claude-Session: https://claude.ai/code/session_01KN565tFAuJUAEkK2DdRF1e
1 parent 65e921e commit 73b6246

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/configs/CacheBlobStorageConfig.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.azure.storage.blob.BlobContainerClient;
44
import com.azure.storage.blob.BlobServiceClientBuilder;
5+
import com.azure.storage.common.policy.RequestRetryOptions;
6+
import com.azure.storage.common.policy.RetryPolicyType;
57
import com.lantanagroup.link.measureeval.services.AbsResourceService;
68
import lombok.Getter;
79
import lombok.Setter;
@@ -13,6 +15,8 @@
1315
import org.springframework.context.annotation.Configuration;
1416
import org.springframework.lang.Nullable;
1517

18+
import java.time.Duration;
19+
1620
@Getter
1721
@Setter
1822
@Configuration
@@ -24,18 +28,46 @@ public class CacheBlobStorageConfig {
2428
private String blobContainerName;
2529
private String blobRoot;
2630

31+
/**
32+
* Azure SDK in-process tries per blob call (initial call + quick retries). The Kafka
33+
* retry-topic ladder owns real retrying; one extra in-process try only absorbs a dropped
34+
* connection. The SDK default of 4 exponential tries hid minutes of silent retrying inside
35+
* every consumer attempt while blocking the single-threaded consumer executor.
36+
*/
37+
private int maxTries = 2;
38+
39+
/** Cap on a single network attempt; the SDK default is effectively unbounded. */
40+
private int tryTimeoutSeconds = 10;
41+
2742
@Bean
2843
@Nullable
2944
public AbsResourceService absResourceService() {
3045
if (StringUtils.isAnyEmpty(connectionString, blobContainerName)) {
3146
logger.info("cache-blob-storage not configured, AbsResourceService disabled");
3247
return null;
3348
}
34-
logger.info("Creating AbsResourceService: container={}, blobRoot={}", blobContainerName, blobRoot);
49+
logger.info("Creating AbsResourceService: container={}, blobRoot={}, maxTries={}, tryTimeout={}s",
50+
blobContainerName, blobRoot, maxTries, tryTimeoutSeconds);
3551
BlobContainerClient client = new BlobServiceClientBuilder()
3652
.connectionString(connectionString)
53+
.retryOptions(buildRetryOptions())
3754
.buildClient()
3855
.getBlobContainerClient(blobContainerName);
3956
return new AbsResourceService(client, blobRoot != null ? blobRoot : "");
4057
}
58+
59+
/**
60+
* Fail-fast policy for the cache blob client: an ABS outage should surface as a quick failure
61+
* that the retry-topic ladder handles visibly (log lines, attempt headers, dead-lettering),
62+
* not as silent in-SDK retrying. Package-private so the test pins the exact values.
63+
*/
64+
RequestRetryOptions buildRetryOptions() {
65+
return new RequestRetryOptions(
66+
RetryPolicyType.FIXED,
67+
maxTries,
68+
Duration.ofSeconds(tryTimeoutSeconds),
69+
Duration.ofSeconds(1),
70+
Duration.ofSeconds(1),
71+
null);
72+
}
4173
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.lantanagroup.link.measureeval.configs;
2+
3+
import com.azure.storage.common.policy.RequestRetryOptions;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.junit.jupiter.api.Assertions.*;
7+
8+
/**
9+
* The resource-cache blob client must fail fast: the Kafka retry-topic ladder owns retrying, and
10+
* the Azure SDK's default policy (4 exponential tries, effectively unbounded per-try timeout) hid
11+
* ~3 minutes of silent retrying inside every consumer attempt while blocking the single-threaded
12+
* consumer executor. These tests pin the tuned policy actually handed to the client builder.
13+
*/
14+
class CacheBlobStorageConfigTest {
15+
16+
@Test
17+
void retryOptions_failFast_defaults() {
18+
CacheBlobStorageConfig config = new CacheBlobStorageConfig();
19+
20+
RequestRetryOptions options = config.buildRetryOptions();
21+
22+
assertEquals(2, options.getMaxTries(),
23+
"one quick in-process retry absorbs a dropped connection; anything more belongs to the ladder");
24+
assertEquals(10, options.getTryTimeoutDuration().toSeconds(),
25+
"each network attempt must be capped, not left to the SDK's effectively unbounded default");
26+
}
27+
28+
@Test
29+
void retryOptions_areConfigurable() {
30+
CacheBlobStorageConfig config = new CacheBlobStorageConfig();
31+
config.setMaxTries(1);
32+
config.setTryTimeoutSeconds(5);
33+
34+
RequestRetryOptions options = config.buildRetryOptions();
35+
36+
assertEquals(1, options.getMaxTries());
37+
assertEquals(5, options.getTryTimeoutDuration().toSeconds());
38+
}
39+
}

0 commit comments

Comments
 (0)