Timeouts, Logging and Fallbacks

Timeouts

Without timeouts a slow product-service makes order-service threads wait for a long time, and soon order-service has no free threads either. Always set a connect timeout (time to open the connection) and a read timeout (time to wait for the response):

spring:
  cloud:
    openfeign:
      client:
        config:
          default:                     # every Feign client
            connect-timeout: 2000      # ms
            read-timeout: 3000
            logger-level: basic        # none | basic | headers | full
          payment-service:             # override for one client (by name)
            read-timeout: 8000         # banks are slow
      circuitbreaker:
        enabled: true                  # enables fallbacks (needs a circuit breaker on the classpath)

logging:
  level:
    com.shopease.order.client: DEBUG   # Feign logs at DEBUG

Example

When product-service takes 5 seconds:

curl -i localhost:8080/api/orders/1001/details

Example output

feign.RetryableException: Read timed out executing GET http://product-service/api/products/7
    at feign.FeignException.errorExecuting(FeignException.java:300)
Caused by: java.net.SocketTimeoutException: Read timed out

Fallbacks

With spring.cloud.openfeign.circuitbreaker.enabled: true and a circuit breaker on the classpath (spring-cloud-starter-circuitbreaker-resilience4j), you can give a client a fallback. A FallbackFactory also receives the cause:

@FeignClient(name = "product-service", path = "/api/products",
             fallbackFactory = ProductClientFallbackFactory.class)
public interface ProductClient {
    @GetMapping("/{id}")
    ProductDto getProduct(@PathVariable("id") Long id);
}

@Component
@Slf4j
class ProductClientFallbackFactory implements FallbackFactory<ProductClient> {

    private final ProductCache cache;   // last known product data (e.g. Redis)

    ProductClientFallbackFactory(ProductCache cache) { this.cache = cache; }

    @Override
    public ProductClient create(Throwable cause) {
        return id -> {
            log.warn("product-service failed for {}: {}", id, cause.toString());
            return cache.find(id).orElseThrow(() -> new ServiceUnavailableException("product-service down"));
        };
    }
}

Note: A fallback must be honest. Showing a cached product name and price on a product page is fine. Placing an order with a cached price or stock value is not; there, failing with a clear 503 is correct.

Exercise

Exercise: user-service is fast but payment-service is slow. Configure Feign so all clients have a 1 s connect timeout and 2 s read timeout, user-service gets 1 s read timeout, payment-service 10 s, and only payment-service logs full request/response bodies.

Show answer
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connect-timeout: 1000
            read-timeout: 2000
            logger-level: basic
          user-service:
            read-timeout: 1000
          payment-service:
            read-timeout: 10000
            logger-level: full
logging:
  level:
    com.shopease.order.client: DEBUG