TradeMarketingPricingLockResilience.java
package com.ecommerce.trade.infrastructure.resilience;
import com.ecommerce.trade.application.exception.TradeError;
import com.ecommerce.trade.application.exception.TradeException;
import com.ecommerce.trade.application.port.MarketingPort.PricingRejectedException;
import com.ecommerce.trade.infrastructure.config.MarketingPricingLockResilienceProperties;
import com.ecommerce.platform.common.transaction.SynchronousBoundaryGuard;
import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.bulkhead.BulkheadConfig;
import io.github.resilience4j.bulkhead.BulkheadFullException;
import io.github.resilience4j.bulkhead.BulkheadRegistry;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Supplier;
@Component
public final class TradeMarketingPricingLockResilience {
public static final String INSTANCE_NAME = "tradeMarketingPricingLock";
private final CircuitBreaker circuitBreaker;
private final Retry retry;
private final Bulkhead bulkhead;
private final Counter bulkheadRejected;
private final Counter circuitRejected;
public TradeMarketingPricingLockResilience(
CircuitBreakerRegistry circuitBreakerRegistry,
RetryRegistry retryRegistry,
BulkheadRegistry bulkheadRegistry,
MeterRegistry meterRegistry,
MarketingPricingLockResilienceProperties properties) {
Objects.requireNonNull(circuitBreakerRegistry, "circuitBreakerRegistry");
Objects.requireNonNull(retryRegistry, "retryRegistry");
Objects.requireNonNull(bulkheadRegistry, "bulkheadRegistry");
Objects.requireNonNull(meterRegistry, "meterRegistry");
validateBudget(properties);
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(properties.maxAttempts())
.waitDuration(properties.retryWait())
.retryOnException(TradeMarketingPricingLockResilience::isRetryable)
.build();
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(properties.slidingWindowSize())
.minimumNumberOfCalls(properties.minimumNumberOfCalls())
.failureRateThreshold(properties.failureRateThreshold())
.waitDurationInOpenState(properties.openStateWait())
.permittedNumberOfCallsInHalfOpenState(properties.halfOpenCalls())
.recordException(TradeMarketingPricingLockResilience::isRecordable)
.ignoreException(TradeMarketingPricingLockResilience::isIgnored)
.build();
BulkheadConfig bulkheadConfig = BulkheadConfig.custom()
.maxConcurrentCalls(properties.maxConcurrentCalls())
.maxWaitDuration(properties.bulkheadMaxWait())
.build();
retry = retryRegistry.retry(INSTANCE_NAME, retryConfig);
circuitBreaker = circuitBreakerRegistry.circuitBreaker(INSTANCE_NAME, circuitBreakerConfig);
bulkhead = bulkheadRegistry.bulkhead(INSTANCE_NAME, bulkheadConfig);
bulkheadRejected = rejectionCounter(meterRegistry, "bulkhead");
circuitRejected = rejectionCounter(meterRegistry, "circuit");
}
public <T> T execute(Supplier<T> remoteCall) {
Objects.requireNonNull(remoteCall, "remoteCall");
SynchronousBoundaryGuard.requireOutsideTransaction(INSTANCE_NAME);
Supplier<T> guarded = Bulkhead.decorateSupplier(bulkhead, remoteCall);
guarded = Retry.decorateSupplier(retry, guarded);
guarded = CircuitBreaker.decorateSupplier(circuitBreaker, guarded);
try {
return guarded.get();
} catch (PricingRejectedException | TradeException exception) {
throw exception;
} catch (BulkheadFullException exception) {
bulkheadRejected.increment();
throw new TradeException(TradeError.REMOTE_DEPENDENCY_UNAVAILABLE, exception);
} catch (CallNotPermittedException exception) {
circuitRejected.increment();
throw new TradeException(TradeError.REMOTE_DEPENDENCY_UNAVAILABLE, exception);
} catch (RuntimeException exception) {
throw new TradeException(TradeError.REMOTE_DEPENDENCY_UNAVAILABLE, exception);
}
}
private static boolean isRetryable(Throwable throwable) {
return throwable instanceof MarketingPricingLockFailure failure && failure.retryable();
}
private static boolean isRecordable(Throwable throwable) {
return throwable instanceof MarketingPricingLockFailure failure && failure.recordable();
}
private static boolean isIgnored(Throwable throwable) {
return throwable instanceof PricingRejectedException
|| throwable instanceof BulkheadFullException
|| throwable instanceof MarketingPricingLockFailure failure && !failure.recordable();
}
private static void validateBudget(MarketingPricingLockResilienceProperties properties) {
Duration maximumAttemptTime = properties.connectTimeout().plus(properties.readTimeout());
Duration maximumRetryWait = properties.retryWait().multipliedBy(properties.maxAttempts() - 1L);
Duration configuredWorstCase = maximumAttemptTime
.multipliedBy(properties.maxAttempts())
.plus(maximumRetryWait)
.plus(properties.bulkheadMaxWait().multipliedBy(properties.maxAttempts()));
if (configuredWorstCase.compareTo(properties.totalBudget()) > 0) {
throw new IllegalArgumentException(
"Trade to Marketing pricing-lock policy can exceed its total budget: worstCase="
+ configuredWorstCase + ", totalBudget=" + properties.totalBudget());
}
}
private static Counter rejectionCounter(MeterRegistry meterRegistry, String guard) {
return Counter.builder("ecommerce.http.client.resilience.rejections")
.description("Synchronous dependency calls rejected before starting remote I/O")
.tag("service", "trade-service")
.tag("dependency", "marketing-service")
.tag("operation", "pricing_lock")
.tag("guard", guard)
.register(meterRegistry);
}
}