diff --git a/README.md b/README.md index b5f4cd5..87f6e4b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Claude Code plugin marketplace with 3 focused plugins for Java developers. All | Plugin | Skills | Commands | Agents | Install when | |---|---|---|---|---| | `java-core` | 14 | 2 | `java-architect`, `java-build-resolver` | Every Java project | -| `java-spring` | 7 | 2 | `java-spring-expert` | Spring Boot projects | +| `java-spring` | 8 | 2 | `java-spring-expert` | Spring Boot projects | | `java-quality` | 3 | 1 | `java-security-reviewer`, `java-performance-reviewer`, `java-test-engineer` | Quality enforcement | ## Quick Setup (5 minutes) @@ -76,6 +76,7 @@ Skills activate automatically based on context, or invoke them explicitly. | `/java-spring:java-security` | Review or generate Spring Security config — JWT, OAuth2, method security, CORS (Boot 2.x & 3.x) | | `/java-spring:java-openapi` | Generate or review OpenAPI/Swagger docs — `@Tag`, `@Operation`, `@Schema`, JWT auth scheme (springdoc v1/v2) | | `/java-spring:java-spring-ai` | Add AI features to Spring Boot — ChatClient, RAG, tool calling, memory (Spring AI 1.x / LangChain4J) | +| `/java-spring:java-resilience` | Add Resilience4J patterns — circuit breaker, retry, rate limiter, bulkhead, timeout (Boot 2.x & 3.x) | ### java-quality diff --git a/plugins/java-spring/skills/java-resilience/SKILL.md b/plugins/java-spring/skills/java-resilience/SKILL.md new file mode 100644 index 0000000..b98e17b --- /dev/null +++ b/plugins/java-spring/skills/java-resilience/SKILL.md @@ -0,0 +1,112 @@ +--- +name: java-resilience +description: Use when the user asks to add resilience patterns, handle service failures, implement circuit breaker, retry, rate limiter, bulkhead, or timeout in a Spring Boot project using Resilience4J. +version: 1.0.0 +authors: [java-plugins contributors] +tags: [java, spring-boot, resilience4j, circuit-breaker, retry, rate-limiter, bulkhead, timeout] +allowed-tools: [Read, Glob, Grep, Edit, Write] +--- + +# Resilience4J Skill + +Detect the existing setup, then apply the correct pattern. + +## Step 1 — Detect setup + +Check `pom.xml` or `build.gradle`: +- `resilience4j-spring-boot3` → Spring Boot 3.x (use `io.github.resilience4j:resilience4j-spring-boot3`) +- `resilience4j-spring-boot2` → Spring Boot 2.x +- `spring-cloud-starter-circuitbreaker-resilience4j` → Spring Cloud Circuit Breaker abstraction +- None present → offer to add (recommend `resilience4j-spring-boot3` for Boot 3.x) + +Check Java version: Java 17+ enables records for fallback DTOs; Java 8+ supported throughout. + +--- + +## Mode: `review` + +User asks to review existing Resilience4J config. Check for: + +- [ ] Circuit breaker thresholds are tuned — `slidingWindowSize`, `failureRateThreshold`, `waitDurationInOpenState` set explicitly (not defaults) +- [ ] `slowCallDurationThreshold` and `slowCallRateThreshold` configured — slow calls should also trip the breaker +- [ ] Fallback methods match the same signature as the guarded method (same return type + `Throwable` param) +- [ ] `@CircuitBreaker` and `@Retry` not stacked without `fallbackMethod` on the outer annotation — will swallow exceptions silently +- [ ] Retry has `ignoreExceptions` for business errors (e.g. `IllegalArgumentException`, `EntityNotFoundException`) — don't retry 4xx errors +- [ ] `RateLimiter` uses `limitForPeriod` appropriate for downstream SLA — not an arbitrary number +- [ ] Bulkhead `maxConcurrentCalls` sized against thread pool or reactive scheduler +- [ ] Actuator endpoints exposed: `management.endpoints.web.exposure.include=health,circuitbreakers,retries,ratelimiters` +- [ ] Circuit breaker state changes logged via `CircuitBreakerEvent` listener — not silent +- [ ] No `@Retry` on `@Transactional` methods — retries after a rolled-back transaction reopen a new transaction + +--- + +## Mode: `circuit-breaker` + +User asks to add a circuit breaker to protect a downstream call. + +1. Add dependency (see `references/patterns.md` → Setup) +2. Configure in `application.yml` — set `sliding-window-size`, `failure-rate-threshold`, `wait-duration-in-open-state` +3. Annotate the method with `@CircuitBreaker(name = "serviceName", fallbackMethod = "fallback")` +4. Write the fallback method — same return type, extra `Throwable` parameter +5. Expose circuit breaker health: `management.health.circuitbreakers.enabled=true` +6. Add event listener to log state transitions (CLOSED→OPEN→HALF_OPEN) + +Version note: +- Spring Boot 3.x → `resilience4j-spring-boot3` +- Spring Boot 2.x → `resilience4j-spring-boot2` + +--- + +## Mode: `retry` + +User asks to add automatic retry for flaky remote calls. + +1. Configure retry in `application.yml` — `max-attempts`, `wait-duration`, `exponential-backoff-multiplier` +2. Set `ignore-exceptions` for non-retryable errors (validation errors, 4xx responses) +3. Set `retry-exceptions` explicitly (e.g. `IOException`, `TimeoutException`) +4. Annotate with `@Retry(name = "serviceName", fallbackMethod = "fallback")` +5. For HTTP clients: check response status before retrying — use `retryOnResultPredicate` to retry on 5xx, not 4xx +6. Warn if stacking with `@CircuitBreaker` — retry fires first, circuit breaker wraps it; set `retry.max-attempts` lower than circuit breaker `sliding-window-size` + +--- + +## Mode: `rate-limiter` + +User asks to limit how often a method can be called (outgoing rate limiting or incoming API protection). + +1. Configure `limit-for-period` (requests per window), `limit-refresh-period`, `timeout-duration` +2. Annotate with `@RateLimiter(name = "serviceName", fallbackMethod = "fallback")` +3. For incoming API protection: place on controller method or use a filter +4. For outgoing: place on the service/client method calling the downstream + +--- + +## Mode: `bulkhead` + +User asks to limit concurrent calls to isolate failures (prevent thread starvation). + +Two types — choose based on context: +- **Semaphore bulkhead** (default): limits concurrent calls via a counter — lightweight, same thread pool +- **ThreadPool bulkhead**: executes in a dedicated pool — true isolation, for blocking I/O + +Configure `max-concurrent-calls` and `max-wait-duration` (semaphore) or pool size (thread pool). +Annotate with `@Bulkhead(name = "serviceName", type = Bulkhead.Type.SEMAPHORE)`. + +--- + +## Mode: `timeout` + +User asks to add a timeout to a method. + +1. Use `@TimeLimiter` for reactive/async methods (returns `CompletableFuture` or `Flux`/`Mono`) +2. For blocking methods: use `@CircuitBreaker` with `slowCallDurationThreshold` instead +3. Configure `timeout-duration` in `application.yml` +4. `@TimeLimiter` requires the method to return `CompletableFuture` — wrap synchronous calls if needed + +--- + +## Output format + +For **review mode**: list findings as `[CRITICAL] / [HIGH] / [MEDIUM] / [LOW]` with file:line references. + +For **implementation modes**: show exact Maven/Gradle snippet, full `application.yml` config block, and complete annotated Java example. State minimum Spring Boot version where relevant. diff --git a/plugins/java-spring/skills/java-resilience/references/patterns.md b/plugins/java-spring/skills/java-resilience/references/patterns.md new file mode 100644 index 0000000..543d38e --- /dev/null +++ b/plugins/java-spring/skills/java-resilience/references/patterns.md @@ -0,0 +1,312 @@ +# Resilience4J — Reference Patterns + +## Setup + +### Maven (Spring Boot 3.x) +```xml + + io.github.resilience4j + resilience4j-spring-boot3 + 2.2.0 + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-actuator + +``` + +### Maven (Spring Boot 2.x) +```xml + + io.github.resilience4j + resilience4j-spring-boot2 + 1.7.1 + + + org.springframework.boot + spring-boot-starter-aop + +``` + +--- + +## Circuit Breaker + +### application.yml +```yaml +resilience4j: + circuitbreaker: + instances: + paymentService: + sliding-window-type: COUNT_BASED + sliding-window-size: 10 + failure-rate-threshold: 50 # trip at 50% failures + slow-call-duration-threshold: 2s + slow-call-rate-threshold: 80 # trip if 80% calls are slow + wait-duration-in-open-state: 30s + permitted-number-of-calls-in-half-open-state: 3 + automatic-transition-from-open-to-half-open-enabled: true + record-exceptions: + - java.io.IOException + - java.util.concurrent.TimeoutException + - feign.FeignException.ServiceUnavailable + ignore-exceptions: + - com.example.exception.BusinessException + +management: + health: + circuitbreakers: + enabled: true + endpoints: + web: + exposure: + include: health,circuitbreakers,retries,ratelimiters +``` + +### Service +```java +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentService { + + private final PaymentClient paymentClient; + + @CircuitBreaker(name = "paymentService", fallbackMethod = "processPaymentFallback") + public PaymentResponse processPayment(PaymentRequest request) { + return paymentClient.process(request); + } + + // Fallback — same return type, extra Throwable param + private PaymentResponse processPaymentFallback(PaymentRequest request, Throwable ex) { + log.warn("Payment service unavailable, using fallback. Cause: {}", ex.getMessage()); + return PaymentResponse.queued(request.getOrderId()); + } +} +``` + +### Event listener (log state transitions) +```java +@Component +@RequiredArgsConstructor +@Slf4j +public class CircuitBreakerEventListener { + + @EventListener + public void onStateTransition(CircuitBreakerOnStateTransitionEvent event) { + log.warn("CircuitBreaker '{}' state: {} → {}", + event.getCircuitBreakerName(), + event.getStateTransition().getFromState(), + event.getStateTransition().getToState()); + } +} +``` + +--- + +## Retry + +### application.yml +```yaml +resilience4j: + retry: + instances: + inventoryService: + max-attempts: 3 + wait-duration: 500ms + enable-exponential-backoff: true + exponential-backoff-multiplier: 2 # 500ms, 1s, 2s + retry-exceptions: + - java.io.IOException + - java.util.concurrent.TimeoutException + ignore-exceptions: + - com.example.exception.ResourceNotFoundException + - com.example.exception.ValidationException +``` + +### Service +```java +@Service +@RequiredArgsConstructor +@Slf4j +public class InventoryService { + + private final InventoryClient inventoryClient; + + @Retry(name = "inventoryService", fallbackMethod = "checkStockFallback") + @CircuitBreaker(name = "inventoryService", fallbackMethod = "checkStockFallback") + public StockResponse checkStock(String sku) { + return inventoryClient.getStock(sku); + } + + private StockResponse checkStockFallback(String sku, Throwable ex) { + log.error("Inventory check failed for SKU: {} after retries. Cause: {}", sku, ex.getMessage()); + return StockResponse.unknown(sku); + } +} +``` + +### Retry on HTTP response status (RestClient / WebClient) +```java +resilience4j: + retry: + instances: + externalApi: + result-predicate: com.example.resilience.ServerErrorPredicate + +// Predicate class +public class ServerErrorPredicate implements Predicate { + @Override + public boolean test(HttpResponse response) { + return response.getStatusCode().is5xxServerError(); + } +} +``` + +--- + +## Rate Limiter + +### application.yml +```yaml +resilience4j: + ratelimiter: + instances: + smsService: + limit-for-period: 10 # 10 requests + limit-refresh-period: 1s # per second + timeout-duration: 500ms # wait up to 500ms for a permit +``` + +### Service +```java +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final SmsClient smsClient; + + @RateLimiter(name = "smsService", fallbackMethod = "sendSmsFallback") + public void sendSms(String phone, String message) { + smsClient.send(phone, message); + } + + private void sendSmsFallback(String phone, String message, RequestNotPermitted ex) { + log.warn("SMS rate limit exceeded for phone: {}", phone); + // Queue for later or drop + } +} +``` + +--- + +## Bulkhead (Semaphore) + +### application.yml +```yaml +resilience4j: + bulkhead: + instances: + reportService: + max-concurrent-calls: 5 # max 5 concurrent calls + max-wait-duration: 100ms # wait 100ms for a slot, then fail +``` + +### Service +```java +@Service +@RequiredArgsConstructor +public class ReportService { + + @Bulkhead(name = "reportService", fallbackMethod = "generateReportFallback") + public ReportResponse generateReport(ReportRequest request) { + // heavy operation + return reportEngine.generate(request); + } + + private ReportResponse generateReportFallback(ReportRequest request, BulkheadFullException ex) { + throw new ServiceBusyException("Report service at capacity, try again later"); + } +} +``` + +--- + +## TimeLimiter (Async Timeout) + +### application.yml +```yaml +resilience4j: + timelimiter: + instances: + asyncService: + timeout-duration: 3s + cancel-running-future: true +``` + +### Service +```java +@Service +@RequiredArgsConstructor +public class AsyncProcessingService { + + private final HeavyProcessor processor; + + @TimeLimiter(name = "asyncService", fallbackMethod = "processFallback") + @CircuitBreaker(name = "asyncService") + public CompletableFuture process(ProcessRequest request) { + return CompletableFuture.supplyAsync(() -> processor.run(request)); + } + + private CompletableFuture processFallback( + ProcessRequest request, TimeoutException ex) { + return CompletableFuture.completedFuture(ProcessResult.timeout()); + } +} +``` + +--- + +## Combining Patterns (recommended order) + +When stacking multiple annotations, the execution order is: +`TimeLimiter → CircuitBreaker → Bulkhead → RateLimiter → Retry` + +```java +@RateLimiter(name = "externalApi") +@Bulkhead(name = "externalApi") +@CircuitBreaker(name = "externalApi", fallbackMethod = "fallback") +@Retry(name = "externalApi") +public ApiResponse callExternalApi(ApiRequest request) { + return externalClient.call(request); +} +``` + +Control order explicitly in `application.yml`: +```yaml +resilience4j: + annotation-aware-order: + - RateLimiter + - Bulkhead + - CircuitBreaker + - TimeLimiter + - Retry +``` + +--- + +## Actuator Endpoints + +``` +GET /actuator/health # includes circuit breaker states +GET /actuator/circuitbreakers # all circuit breakers + metrics +GET /actuator/circuitbreakerevents # recent events (state changes, calls) +GET /actuator/retries # retry stats +GET /actuator/ratelimiters # rate limiter stats +```