-
Notifications
You must be signed in to change notification settings - Fork 0
Annotation Reference
Complete reference for @DistributedLock, @DistributedSemaphore, @RateLimit, and related types.
The main annotation for distributed locking.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key();
LockType type() default LockType.REENTRANT;
AcquisitionMode mode() default AcquisitionMode.SKIP_IMMEDIATELY;
String leaseTime() default "";
String waitTime() default "";
boolean autoRenew() default false;
Class<? extends LockSkipHandler> skipHandler() default LockThrowExceptionHandler.class;
LeaseExpirationBehavior onLeaseExpired() default LeaseExpirationBehavior.LOG_WARNING;
}The lock identifier. Supports SpEL expressions.
// Static key
@DistributedLock(key = "my-lock")
// Dynamic key with SpEL
@DistributedLock(key = "#{#userId}")
@DistributedLock(key = "#{'order-' + #orderId}")
@DistributedLock(key = "#{#user.id}")The final Redis key is: {locksmith.lock.key-prefix}{key}
| Config Prefix | Annotation Key | Redis Key |
|---|---|---|
lock: |
task |
lock:task |
myapp: |
user-123 |
myapp:user-123 |
lock: |
#{#orderId} (="ABC") |
lock:ABC |
The type of lock to acquire.
| Value | Description |
|---|---|
REENTRANT |
Exclusive lock, same thread can re-enter (default) |
READ |
Shared lock, multiple readers allowed |
WRITE |
Exclusive lock, blocks readers and writers |
@DistributedLock(key = "resource", type = LockType.REENTRANT)
@DistributedLock(key = "resource", type = LockType.READ)
@DistributedLock(key = "resource", type = LockType.WRITE)See: Lock Types
How to handle lock acquisition.
| Value | Description |
|---|---|
SKIP_IMMEDIATELY |
Fail instantly if lock is held (default) |
WAIT_AND_SKIP |
Wait up to waitTime before failing |
// Fail immediately
@DistributedLock(key = "task", mode = AcquisitionMode.SKIP_IMMEDIATELY)
// Wait then fail
@DistributedLock(
key = "task",
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s"
)How long the lock is held before auto-release.
| Value | Meaning |
|---|---|
"" (empty) |
Use global config (default) |
| Duration string | Override for this method |
@DistributedLock(key = "task") // Uses config default
@DistributedLock(key = "task", leaseTime = "5m") // 5 minutes
@DistributedLock(key = "task", leaseTime = "30s") // 30 seconds
@DistributedLock(key = "task", leaseTime = "PT1H") // 1 hour (ISO-8601)Duration formats:
- Simple:
500ms,30s,5m,1h - ISO-8601:
PT30S,PT5M,PT1H
Note: Ignored when
autoRenew = true
How long to wait in WAIT_AND_SKIP mode.
| Value | Meaning |
|---|---|
"" (empty) |
Use global config (default) |
| Duration string | Override for this method |
@DistributedLock(
key = "task",
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s"
)Only effective when mode = WAIT_AND_SKIP.
Enable automatic lease renewal via Redisson's watchdog.
| Value | Description |
|---|---|
false |
Use fixed lease time (default) |
true |
Auto-extend lock during execution |
@DistributedLock(key = "long-task", autoRenew = true)When enabled:
-
leaseTimeis ignored -
onLeaseExpiredhas no effect - Lock is held until method completes
Handler invoked when lock acquisition fails. Handlers are resolved as Spring beans first (supporting dependency injection), with reflection fallback.
| Value | Description |
|---|---|
LockThrowExceptionHandler.class |
Throw LockNotAcquiredException (default) |
LockReturnDefaultHandler.class |
Return null/0/false/Optional.empty() |
| Custom class | Your implementation (can be a @Component) |
@DistributedLock(key = "task") // Throws exception
@DistributedLock(key = "task", skipHandler = LockReturnDefaultHandler.class)
@DistributedLock(key = "task", skipHandler = MyCustomHandler.class) // Can be @Component with DISee: Skip Handlers
Behavior when execution exceeds lease time.
| Value | Description |
|---|---|
LOG_WARNING |
Log a warning message (default) |
THROW_EXCEPTION |
Throw LeaseExpiredException
|
IGNORE |
Silently ignore |
@DistributedLock(key = "task", onLeaseExpired = LeaseExpirationBehavior.LOG_WARNING)
@DistributedLock(key = "task", onLeaseExpired = LeaseExpirationBehavior.THROW_EXCEPTION)
@DistributedLock(key = "task", onLeaseExpired = LeaseExpirationBehavior.IGNORE)Note: Has no effect when
autoRenew = true
See: Lease Expiration Detection
Annotation for permit-based concurrency control.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedSemaphore {
String key();
int permits() default 1;
AcquisitionMode mode() default AcquisitionMode.SKIP_IMMEDIATELY;
String leaseTime() default "";
String waitTime() default "";
Class<? extends SemaphoreSkipHandler> skipHandler() default SemaphoreThrowExceptionHandler.class;
LeaseExpirationBehavior onLeaseExpired() default LeaseExpirationBehavior.LOG_WARNING;
}The semaphore identifier. Supports SpEL expressions.
// Static key
@DistributedSemaphore(key = "api-pool", permits = 5)
// Dynamic key with SpEL
@DistributedSemaphore(key = "#{#userId}", permits = 3)
@DistributedSemaphore(key = "#{'tenant-' + #tenantId}", permits = 10)The final Redis key is: {locksmith.semaphore.key-prefix}{key}
Maximum number of concurrent executions allowed.
@DistributedSemaphore(key = "pool", permits = 5) // 5 concurrent
@DistributedSemaphore(key = "pool", permits = 10) // 10 concurrent
@DistributedSemaphore(key = "pool", permits = 1) // 1 concurrent (like a lock)Important: Must be a positive integer. When the same key is used across multiple methods, the
permitsvalue must be consistent.
How to handle permit acquisition.
| Value | Description |
|---|---|
SKIP_IMMEDIATELY |
Fail instantly if no permit available (default) |
WAIT_AND_SKIP |
Wait up to waitTime before failing |
// Fail immediately
@DistributedSemaphore(key = "pool", permits = 5, mode = AcquisitionMode.SKIP_IMMEDIATELY)
// Wait then fail
@DistributedSemaphore(
key = "pool",
permits = 5,
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s"
)How long a permit is held before auto-release.
| Value | Meaning |
|---|---|
"" (empty) |
Use global config (default) |
| Duration string | Override for this method |
@DistributedSemaphore(key = "pool", permits = 5) // Uses config default
@DistributedSemaphore(key = "pool", permits = 5, leaseTime = "5m") // 5 minutes
@DistributedSemaphore(key = "pool", permits = 5, leaseTime = "30s") // 30 secondsHow long to wait in WAIT_AND_SKIP mode.
| Value | Meaning |
|---|---|
"" (empty) |
Use global config (default) |
| Duration string | Override for this method |
@DistributedSemaphore(
key = "pool",
permits = 5,
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s"
)Only effective when mode = WAIT_AND_SKIP.
Handler invoked when permit acquisition fails. Handlers are resolved as Spring beans first (supporting dependency injection), with reflection fallback.
| Value | Description |
|---|---|
SemaphoreThrowExceptionHandler.class |
Throw SemaphoreNotAcquiredException (default) |
SemaphoreReturnDefaultHandler.class |
Return null/0/false/Optional.empty() |
| Custom class | Your implementation (can be a @Component) |
@DistributedSemaphore(key = "pool", permits = 5) // Throws exception
@DistributedSemaphore(key = "pool", permits = 5, skipHandler = SemaphoreReturnDefaultHandler.class)
@DistributedSemaphore(key = "pool", permits = 5, skipHandler = MySemaphoreHandler.class) // Can be @Component with DISee: Skip Handlers
Behavior when execution exceeds lease time.
| Value | Description |
|---|---|
LOG_WARNING |
Log a warning message (default) |
THROW_EXCEPTION |
Throw SemaphoreLeaseExpiredException
|
IGNORE |
Silently ignore |
@DistributedSemaphore(key = "pool", permits = 5, onLeaseExpired = LeaseExpirationBehavior.LOG_WARNING)
@DistributedSemaphore(key = "pool", permits = 5, onLeaseExpired = LeaseExpirationBehavior.THROW_EXCEPTION)
@DistributedSemaphore(key = "pool", permits = 5, onLeaseExpired = LeaseExpirationBehavior.IGNORE)See: Lease Expiration Detection
Annotation for rate limiting - controlling request throughput over time.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key();
long permits() default 10;
String interval() default "1s";
RateType type() default RateType.OVERALL;
AcquisitionMode mode() default AcquisitionMode.SKIP_IMMEDIATELY;
String waitTime() default "";
Class<? extends RateLimitSkipHandler> skipHandler() default RateLimitThrowExceptionHandler.class;
}The rate limiter identifier. Supports SpEL expressions.
// Static key
@RateLimit(key = "api-endpoint", permits = 100, interval = "1m")
// Dynamic key with SpEL
@RateLimit(key = "#{#userId}", permits = 100, interval = "1m")
@RateLimit(key = "#{'api-' + #region}", permits = 50, interval = "1m")The final Redis key is: {locksmith.rate-limit.key-prefix}{key}
Number of permits allowed per interval.
| Value | Meaning |
|---|---|
10 (default) |
10 requests per interval |
| Any positive long | Custom limit |
@RateLimit(key = "api", permits = 100) // 100 per interval
@RateLimit(key = "api", permits = 1000) // 1000 per intervalTime window for the permits.
| Value | Meaning |
|---|---|
"1s" (default) |
1 second |
| Duration string | Custom interval |
@RateLimit(key = "api", permits = 100, interval = "1s") // 100/second
@RateLimit(key = "api", permits = 100, interval = "1m") // 100/minute
@RateLimit(key = "api", permits = 1000, interval = "1h") // 1000/hour
@RateLimit(key = "api", permits = 5, interval = "500ms") // 5 per 500msDuration formats:
- Simple:
500ms,30s,5m,1h - ISO-8601:
PT30S,PT5M,PT1H
The scope of rate limiting.
| Value | Description |
|---|---|
OVERALL |
Permits shared across all clients/instances (default) |
PER_CLIENT |
Each Redisson client instance gets its own quota |
// Shared across all instances
@RateLimit(key = "api", permits = 100, interval = "1m", type = RateType.OVERALL)
// Each instance gets its own limit
@RateLimit(key = "api", permits = 100, interval = "1m", type = RateType.PER_CLIENT)See: Rate Limiting
How to handle permit acquisition.
| Value | Description |
|---|---|
SKIP_IMMEDIATELY |
Fail instantly if rate limit exceeded (default) |
WAIT_AND_SKIP |
Wait up to waitTime before failing |
// Fail immediately
@RateLimit(key = "api", permits = 100, interval = "1m", mode = AcquisitionMode.SKIP_IMMEDIATELY)
// Wait then fail
@RateLimit(
key = "api",
permits = 100,
interval = "1m",
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "5s"
)How long to wait in WAIT_AND_SKIP mode.
| Value | Meaning |
|---|---|
"" (empty) |
Use global config (default) |
| Duration string | Override for this method |
@RateLimit(
key = "api",
permits = 100,
interval = "1m",
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "5s"
)Only effective when mode = WAIT_AND_SKIP.
Handler invoked when rate limit is exceeded. Handlers are resolved as Spring beans first (supporting dependency injection), with reflection fallback.
| Value | Description |
|---|---|
RateLimitThrowExceptionHandler.class |
Throw RateLimitExceededException (default) |
RateLimitReturnDefaultHandler.class |
Return null/0/false/Optional.empty() |
| Custom class | Your implementation (can be a @Component) |
@RateLimit(key = "api", permits = 100, interval = "1m") // Throws exception
@RateLimit(key = "api", permits = 100, interval = "1m", skipHandler = RateLimitReturnDefaultHandler.class)
@RateLimit(key = "api", permits = 100, interval = "1m", skipHandler = MyCustomHandler.class)See: Skip Handlers
public enum LockType {
REENTRANT, // Exclusive, reentrant lock
READ, // Shared read lock
WRITE // Exclusive write lock
}Used by @DistributedLock, @DistributedSemaphore, and @RateLimit.
public enum AcquisitionMode {
SKIP_IMMEDIATELY, // Fail instantly if locked/no permit
WAIT_AND_SKIP // Wait up to waitTime
}Used by both @DistributedLock and @DistributedSemaphore.
public enum LeaseExpirationBehavior {
LOG_WARNING, // Log warning message
THROW_EXCEPTION, // Throw LeaseExpiredException/SemaphoreLeaseExpiredException
IGNORE // Do nothing
}Used by @RateLimit to determine how permits are distributed. This enum is provided by Redisson (org.redisson.api.RateType), not by Locksmith.
// org.redisson.api.RateType
public enum RateType {
OVERALL, // Permits shared across all clients/instances (default)
PER_CLIENT // Each Redisson client instance gets its own quota
}public interface LockSkipHandler {
/**
* Handle lock acquisition failure.
*
* @param context Information about the lock and method
* @return Value to return from the method, or throw exception
*/
Object handle(LockContext context);
}import in.riido.locksmith.models.LockContext;
public record LockContext(
String lockKey, // Full Redis key
String methodName, // Formatted method name
Method method, // Reflection Method object
Object[] args, // Method arguments
Class<?> returnType // Method return type
) { }public interface SemaphoreSkipHandler {
/**
* Handle permit acquisition failure.
*
* @param context Information about the semaphore and method
* @return Value to return from the method, or throw exception
*/
Object handle(SemaphoreContext context);
}import in.riido.locksmith.models.SemaphoreContext;
public record SemaphoreContext(
String semaphoreKey, // Full Redis key
String methodName, // Formatted method name
Method method, // Reflection Method object
Object[] args, // Method arguments
Class<?> returnType, // Method return type
String permitId // Permit ID if acquired, null otherwise
) { }public interface RateLimitSkipHandler {
/**
* Handle rate limit exceeded.
*
* @param context Information about the rate limiter and method
* @return Value to return from the method, or throw exception
*/
Object handle(RateLimitContext context);
}import in.riido.locksmith.models.RateLimitContext;
public record RateLimitContext(
String rateLimitKey, // Full Redis key
String methodName, // Formatted method name
Method method, // Reflection Method object
Object[] args, // Method arguments
Class<?> returnType // Method return type
) { }Thrown by LockThrowExceptionHandler when lock acquisition fails.
public class LockNotAcquiredException extends RuntimeException {
public String getLockKey() { }
public String getMethodName() { }
}Thrown when onLeaseExpired = THROW_EXCEPTION and lock execution exceeds lease time.
public class LeaseExpiredException extends RuntimeException {
public String getLockKey() { }
public String getMethodName() { }
public long getLeaseTimeMs() { }
public long getExecutionTimeMs() { }
}Thrown by SemaphoreThrowExceptionHandler when permit acquisition fails.
public class SemaphoreNotAcquiredException extends RuntimeException {
public String getSemaphoreKey() { }
public String getMethodName() { }
}Thrown when onLeaseExpired = THROW_EXCEPTION and semaphore execution exceeds lease time.
public class SemaphoreLeaseExpiredException extends RuntimeException {
public String getSemaphoreKey() { }
public String getMethodName() { }
public long getLeaseTimeMs() { }
public long getExecutionTimeMs() { }
}Thrown when semaphore configuration is invalid (e.g., same key with different permits).
public class SemaphoreConfigurationException extends RuntimeException {
public String getSemaphoreKey() { }
}Thrown by RateLimitThrowExceptionHandler when rate limit is exceeded.
public class RateLimitExceededException extends RuntimeException {
public String getRateLimitKey() { }
public String getMethodName() { }
}Thrown when rate limiter configuration is invalid (e.g., non-positive permits, zero/negative interval).
public class RateLimitConfigurationException extends RuntimeException {
public String getRateLimitKey() { }
}public record LocksmithProperties(
LockProperties lock,
SemaphoreProperties semaphore,
RateLimitProperties rateLimit
) { }
public record LockProperties(
Boolean enabled, // Default: true - when false, @DistributedLock has no effect
Duration leaseTime, // Default: 10 minutes
Duration waitTime, // Default: 60 seconds
String keyPrefix, // Default: "lock:"
Boolean debug, // Default: false
Boolean metricsEnabled // Default: false
) { }
public record SemaphoreProperties(
Boolean enabled, // Default: true - when false, @DistributedSemaphore has no effect
Duration leaseTime, // Default: 5 minutes
Duration waitTime, // Default: 60 seconds
String keyPrefix, // Default: "semaphore:"
Boolean debug, // Default: false
Boolean metricsEnabled // Default: false
) { }
public record RateLimitProperties(
Boolean enabled, // Default: true - when false, @RateLimit has no effect
Duration waitTime, // Default: 60 seconds
String keyPrefix, // Default: "ratelimit:"
Boolean debug, // Default: false
Boolean metricsEnabled // Default: false
) { }YAML configuration:
locksmith:
lock:
enabled: true
lease-time: 10m
wait-time: 60s
key-prefix: "lock:"
debug: false
metrics-enabled: false
semaphore:
enabled: true
lease-time: 5m
wait-time: 60s
key-prefix: "semaphore:"
debug: false
metrics-enabled: false
rate-limit:
enabled: true
wait-time: 60s
key-prefix: "ratelimit:"
debug: false
metrics-enabled: falseSee: Configuration
@Service
public class OrderService {
@DistributedLock(
key = "#{'order-' + #orderId}",
type = LockType.REENTRANT,
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s",
leaseTime = "10m",
skipHandler = LockThrowExceptionHandler.class,
onLeaseExpired = LeaseExpirationBehavior.THROW_EXCEPTION
)
public Order processOrder(String orderId) {
// Process with all options specified
return doProcess(orderId);
}
// Minimal form with defaults
@DistributedLock(key = "#{'order-' + #orderId}")
public Order processOrderSimple(String orderId) {
return doProcess(orderId);
}
}@Service
public class ApiService {
@DistributedSemaphore(
key = "#{'api-' + #region}",
permits = 10,
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "30s",
leaseTime = "5m",
skipHandler = SemaphoreThrowExceptionHandler.class,
onLeaseExpired = LeaseExpirationBehavior.LOG_WARNING
)
public Response callApi(String region) {
// Call with all options specified
return doApiCall(region);
}
// Minimal form with defaults
@DistributedSemaphore(key = "api-pool", permits = 5)
public Response callApiSimple() {
return doApiCall("default");
}
}@Service
public class RateLimitedService {
@RateLimit(
key = "#{'user-' + #userId}",
permits = 100,
interval = "1m",
type = RateType.OVERALL,
mode = AcquisitionMode.WAIT_AND_SKIP,
waitTime = "5s",
skipHandler = RateLimitThrowExceptionHandler.class
)
public Response processUserRequest(String userId) {
// Call with all options specified
return doProcess(userId);
}
// Minimal form with defaults (10 permits per second)
@RateLimit(key = "api-endpoint")
public Response processRequestSimple() {
return doProcess("default");
}
}