-
Notifications
You must be signed in to change notification settings - Fork 0
Skip Handlers
Skip handlers define what happens when a lock, semaphore permit, or rate limit cannot be acquired. Locksmith provides built-in handlers and supports custom implementations for locks, semaphores, and rate limits.
When acquisition fails:
- Locksmith creates a context object (
LockContextorSemaphoreContext) with method details - The configured skip handler's
handle()method is invoked - The handler returns a value (or throws an exception)
- That value becomes the method's return value
Acquisition Failed
│
▼
┌─────────────┐
│ Context │
│ - key │
│ - method │
│ - args │
│ - returnType│
└─────────────┘
│
▼
┌─────────────┐
│ SkipHandler │
│ .handle() │
└─────────────┘
│
▼
Return Value or Exception
Throws LockNotAcquiredException:
@DistributedLock(key = "task") // Uses LockThrowExceptionHandler by default
public void task() { }
// Equivalent to:
@DistributedLock(key = "task", skipHandler = LockThrowExceptionHandler.class)
public void task() { }Calling code must handle the exception:
try {
service.task();
} catch (LockNotAcquiredException e) {
log.warn("Lock not acquired: {}", e.getLockKey());
// Retry, return cached value, etc.
}Returns type-appropriate default values:
@DistributedLock(key = "task", skipHandler = LockReturnDefaultHandler.class)
public String getData() {
return fetchData();
}
// If lock not acquired: returns nullDefault values by type:
| Return Type | Default Value |
|---|---|
| Objects | null |
boolean / Boolean
|
false |
byte / Byte
|
0 |
short / Short
|
0 |
int / Integer
|
0 |
long / Long
|
0L |
float / Float
|
0.0f |
double / Double
|
0.0 |
char / Character
|
'\0' |
Optional |
Optional.empty() |
void |
null |
The context passed to lock skip handlers:
import in.riido.locksmith.models.LockContext;
public record LockContext(
String lockKey, // Full Redis key: "lock:my-task"
String methodName, // Formatted: "MyService.myMethod"
Method method, // java.lang.reflect.Method
Object[] args, // Method arguments
Class<?> returnType // Method return type
) { }Implement LockSkipHandler for custom behavior:
public interface LockSkipHandler {
Object handle(LockContext context);
}public class FallbackValueHandler implements LockSkipHandler {
@Override
public Object handle(LockContext context) {
if (context.returnType() == List.class) {
return Collections.emptyList();
}
if (context.returnType() == Optional.class) {
return Optional.empty();
}
return null;
}
}
@DistributedLock(key = "users", skipHandler = FallbackValueHandler.class)
public List<User> getUsers() {
return userRepository.findAll();
}
// If lock not acquired: returns empty list instead of nullpublic class AlertingHandler implements LockSkipHandler {
private static final Logger log = LoggerFactory.getLogger(AlertingHandler.class);
@Override
public Object handle(LockContext context) {
// Log for monitoring
log.warn("Lock acquisition failed: {} for method {}",
context.lockKey(), context.methodName());
// Send metric
Metrics.counter("lock.acquisition.failed",
"key", context.lockKey()).increment();
// Still throw exception
throw new LockNotAcquiredException(context.lockKey(), context.methodName());
}
}Throws SemaphoreNotAcquiredException:
@DistributedSemaphore(key = "pool", permits = 5) // Uses SemaphoreThrowExceptionHandler by default
public void task() { }
// Equivalent to:
@DistributedSemaphore(key = "pool", permits = 5, skipHandler = SemaphoreThrowExceptionHandler.class)
public void task() { }Calling code must handle the exception:
try {
service.task();
} catch (SemaphoreNotAcquiredException e) {
log.warn("No permit available: {}", e.getSemaphoreKey());
// Retry, return cached value, etc.
}Returns type-appropriate default values (same as LockReturnDefaultHandler):
@DistributedSemaphore(key = "pool", permits = 5, skipHandler = SemaphoreReturnDefaultHandler.class)
public String getData() {
return fetchData();
}
// If no permit available: returns nullThe context passed to semaphore skip handlers:
import in.riido.locksmith.models.SemaphoreContext;
public record SemaphoreContext(
String semaphoreKey, // Full Redis key: "semaphore:my-pool"
String methodName, // Formatted: "MyService.myMethod"
Method method, // java.lang.reflect.Method
Object[] args, // Method arguments
Class<?> returnType, // Method return type
String permitId // Permit ID if acquired, null otherwise
) { }Implement SemaphoreSkipHandler for custom behavior:
public interface SemaphoreSkipHandler {
Object handle(SemaphoreContext context);
}public class RateLimitHandler implements SemaphoreSkipHandler {
@Override
public Object handle(SemaphoreContext context) {
log.warn("Rate limited: {} for method {}",
context.semaphoreKey(), context.methodName());
// Return a rate limit response
return new ApiResponse(429, "Too Many Requests");
}
}
@DistributedSemaphore(key = "api-calls", permits = 10, skipHandler = RateLimitHandler.class)
public ApiResponse callExternalApi() {
return httpClient.get("/data");
}public class QueueSemaphoreHandler implements SemaphoreSkipHandler {
private final Queue<Runnable> retryQueue;
public QueueSemaphoreHandler() {
this.retryQueue = new ConcurrentLinkedQueue<>();
}
@Override
public Object handle(SemaphoreContext context) {
log.info("No permit available, queuing {} for later", context.methodName());
// Queue the operation for later
retryQueue.add(() -> {
// Re-invoke logic here
});
return null;
}
}Throws RateLimitExceededException:
@RateLimit(key = "api", permits = 100, interval = "1m") // Uses RateLimitThrowExceptionHandler by default
public void api() { }
// Equivalent to:
@RateLimit(key = "api", permits = 100, interval = "1m", skipHandler = RateLimitThrowExceptionHandler.class)
public void api() { }Calling code must handle the exception:
try {
service.api();
} catch (RateLimitExceededException e) {
log.warn("Rate limit exceeded: {}", e.getRateLimitKey());
// Retry later, return cached value, etc.
}Returns type-appropriate default values (same as LockReturnDefaultHandler):
@RateLimit(key = "api", permits = 100, interval = "1m", skipHandler = RateLimitReturnDefaultHandler.class)
public String getData() {
return fetchData();
}
// If rate limit exceeded: returns nullThe context passed to rate limit skip handlers:
import in.riido.locksmith.models.RateLimitContext;
public record RateLimitContext(
String rateLimitKey, // Full Redis key: "ratelimit:my-api"
String methodName, // Formatted: "MyService.myMethod"
Method method, // java.lang.reflect.Method
Object[] args, // Method arguments
Class<?> returnType // Method return type
) { }Implement RateLimitSkipHandler for custom behavior:
public interface RateLimitSkipHandler {
Object handle(RateLimitContext context);
}public class Http429Handler implements RateLimitSkipHandler {
@Override
public Object handle(RateLimitContext context) {
log.warn("Rate limit exceeded: {} for method {}",
context.rateLimitKey(), context.methodName());
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", "60")
.body("Rate limit exceeded. Please retry later.");
}
}
@RateLimit(key = "public-api", permits = 100, interval = "1m", skipHandler = Http429Handler.class)
public ResponseEntity<?> publicApi() {
return ResponseEntity.ok(data);
}public class QueueRateLimitHandler implements RateLimitSkipHandler {
private final RetryQueue retryQueue;
public QueueRateLimitHandler(RetryQueue retryQueue) {
this.retryQueue = retryQueue;
}
@Override
public Object handle(RateLimitContext context) {
log.info("Rate limited, queuing {} for later", context.methodName());
retryQueue.enqueue(context.args());
return new AsyncResult("Request queued for processing");
}
}Works for both locks and semaphores:
public class GracefulDegradationHandler implements LockSkipHandler {
@Override
public Object handle(LockContext context) {
log.info("Gracefully degrading {} due to lock contention",
context.methodName());
// Return stale data, cached value, or partial result
return getStaleData(context);
}
private Object getStaleData(LockContext context) {
// Implementation
return null;
}
}public class CircuitBreakerHandler implements SemaphoreSkipHandler {
private final CircuitBreaker circuitBreaker;
public CircuitBreakerHandler() {
this.circuitBreaker = CircuitBreaker.ofDefaults("api-circuit");
}
@Override
public Object handle(SemaphoreContext context) {
// Trip circuit breaker after repeated failures
circuitBreaker.recordFailure();
if (circuitBreaker.isOpen()) {
return getFallback(context);
}
throw new SemaphoreNotAcquiredException(context.semaphoreKey(), context.methodName());
}
private Object getFallback(SemaphoreContext context) {
return null;
}
}public class SmartHandler implements LockSkipHandler {
@Override
public Object handle(LockContext context) {
String methodName = context.method().getName();
switch (methodName) {
case "getUsers":
return Collections.emptyList();
case "getCount":
return 0;
case "isEnabled":
return false;
default:
throw new LockNotAcquiredException(
context.lockKey(), context.methodName());
}
}
}public class ArgumentAwareHandler implements SemaphoreSkipHandler {
@Override
public Object handle(SemaphoreContext context) {
Object[] args = context.args();
if (args.length > 0 && args[0] instanceof String userId) {
log.warn("Could not acquire permit for user: {}", userId);
return getCachedUserData(userId);
}
return null;
}
private Object getCachedUserData(String userId) {
// Fetch from cache
return null;
}
}Locksmith fully supports Spring dependency injection for skip handlers. Handler resolution follows this order:
- Spring Bean Lookup - First looks up the handler as a Spring bean by type from ApplicationContext
- Reflection Fallback - If not found as a bean, instantiates via reflection (requires public no-arg constructor)
This means you can define handlers as Spring @Component beans with full dependency injection support:
@Component
public class AlertingSkipHandler implements LockSkipHandler {
private final AlertService alertService;
private final MetricRegistry metrics;
// Constructor injection - fully supported!
public AlertingSkipHandler(AlertService alertService, MetricRegistry metrics) {
this.alertService = alertService;
this.metrics = metrics;
}
@Override
public Object handle(LockContext context) {
// Use injected services
alertService.sendAlert("Lock acquisition failed: " + context.lockKey());
metrics.counter("lock.skipped").increment();
return null;
}
}
// Usage - just reference the class, Spring handles instantiation
@DistributedLock(key = "task", skipHandler = AlertingSkipHandler.class)
public void criticalTask() { }@Component
public class CacheFallbackHandler implements SemaphoreSkipHandler {
private final CacheManager cacheManager;
public CacheFallbackHandler(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Object handle(SemaphoreContext context) {
Cache cache = cacheManager.getCache("fallback");
return cache.get(context.semaphoreKey(), context.returnType());
}
}
@DistributedSemaphore(key = "api", permits = 10, skipHandler = CacheFallbackHandler.class)
public ApiResponse callExternalApi() { }For simple handlers that don't need Spring beans, just implement the interface with a no-arg constructor:
// No @Component needed - will be instantiated via reflection
public class LoggingSkipHandler implements LockSkipHandler {
@Override
public Object handle(LockContext context) {
System.out.println("Lock acquisition failed for: " + context.lockKey());
return null;
}
}Note: Handler instances are cached per type for performance. Ensure your handlers are stateless and thread-safe.
| Scenario | Lock Handler | Semaphore Handler | Rate Limit Handler |
|---|---|---|---|
| Critical operations | LockThrowExceptionHandler |
SemaphoreThrowExceptionHandler |
RateLimitThrowExceptionHandler |
| Scheduled jobs | LockReturnDefaultHandler |
SemaphoreReturnDefaultHandler |
RateLimitReturnDefaultHandler |
| User-facing with fallback | Custom handler | Custom handler | Custom handler |
| API endpoints | - | - | Custom HTTP 429 handler |
| Monitoring required | Custom handler with metrics | Custom handler with metrics | Custom handler with metrics |
// Good - fast
public Object handle(LockContext context) {
return cachedValue;
}
// Bad - slow
public Object handle(LockContext context) {
return expensiveRemoteCall(); // Don't do this!
}public Object handle(LockContext context) {
Class<?> type = context.returnType();
if (type == void.class) return null;
if (type == Optional.class) return Optional.empty();
if (List.class.isAssignableFrom(type)) return Collections.emptyList();
if (Map.class.isAssignableFrom(type)) return Collections.emptyMap();
// ... handle other types
return null; // Fallback
}public Object handle(LockContext context) {
// INFO for expected skips
log.info("Skipping {} - lock held", context.methodName());
// WARN for unexpected situations
log.warn("Could not acquire critical lock: {}", context.lockKey());
return null;
}Handlers are cached and reused. Do not store state in instance variables:
// Bad - not thread-safe
public class BadHandler implements LockSkipHandler {
private int counter = 0; // Don't do this!
@Override
public Object handle(LockContext context) {
counter++; // Race condition!
return null;
}
}
// Good - thread-safe
public class GoodHandler implements LockSkipHandler {
@Override
public Object handle(LockContext context) {
// Use local variables or thread-safe structures
return null;
}
}- Annotation Reference - Complete attribute documentation
- Troubleshooting - Common issues and solutions
