Introduction
The Distributed Lock plugin provides a framework and interface for a synchronization mechanism that can be distributed outside the context of the app container. In today’s world of horizontal computational scale and massive concurrency, it becomes increasingly difficult to synchronize operations outside the context of a single computational space (server/process/container).
Key Features
-
Distributed Synchronization: Coordinate operations across multiple servers or processes
-
Multiple Providers: Support for Redis and GORM-based locking
-
Flexible Configuration: Configure timeouts, TTLs, and namespaces
-
Domain-based Locking: Convenient methods to lock based on GORM domain instances
-
Automatic Expiration: Lock TTLs prevent deadlocks from crashed processes
-
Thread-safe: Safe for use in concurrent environments
Supported Providers
-
Redis - High-performance distributed locking via Redis
-
GORM - Database-backed locking using Hibernate (MySQL, PostgreSQL, etc.)
Installation
Grails 7
Add the plugin to your build.gradle:
dependencies {
implementation 'cloud.wondrify:distributed-lock:7.0.0'
// Choose your provider:
// For GORM-based locking:
implementation 'cloud.wondrify:distributed-lock-gorm:7.0.0'
// For Redis-based locking (requires grails-redis):
implementation 'org.grails.plugins:grails-redis:3.0.0'
}
Grails 6.x and Earlier
For Grails 6.x and earlier versions, use the previous artifact coordinates:
dependencies {
implementation 'com.bertramlabs.plugins:distributed-lock:6.x.x'
}
Configuration
Redis Provider
First, configure your Redis connection in application.yml:
grails:
redis:
host: localhost
port: 6379
Or in application.groovy:
grails {
redis {
host = 'localhost'
port = 6379
}
}
Then configure the distributed lock options:
YAML Configuration (application.yml)
distributedLock:
provider:
type: RedisLockProvider
# connection: 'otherThanDefault' # Optional: for multi-connection setups
raiseError: true
defaultTimeout: 10000 # 10 seconds
defaultTTL: 3600000 # 1 hour
namespace: 'my-app'
Groovy Configuration (application.groovy)
distributedLock {
provider {
type = RedisLockProvider
// connection = 'otherThanDefault' // Optional
}
raiseError = true
defaultTimeout = 10000l // 10 seconds
defaultTTL = 3600000l // 1 hour
namespace = 'my-app'
}
GORM Provider
For GORM-based locking, configure as follows:
YAML Configuration (application.yml)
distributedLock:
provider:
type: GormLockProvider
raiseError: true
defaultTimeout: 10000
defaultTTL: 3600000
namespace: 'my-app'
Groovy Configuration (application.groovy)
distributedLock {
provider {
type = GormLockProvider
}
raiseError = true
defaultTimeout = 10000l
defaultTTL = 3600000l
namespace = 'my-app'
}
The GORM provider automatically creates a distributed_lock table in your database. This approach is very efficient and can reduce dependency overhead for MySQL, PostgreSQL, and other Hibernate-compatible databases.
Configuration Options
-
provider.type: The implementation class of the lock provider (
RedisLockProviderorGormLockProvider) -
provider.connection: (Redis only) Specify a specific Redis connection for multi-connection setups
-
raiseError: Whether to throw exceptions on failures (default:
true) or return boolean status -
namespace: A namespace prefix for lock keys (default:
'distributed-lock') -
defaultTimeout: Default time in milliseconds to wait for lock acquisition (default:
30000) -
defaultTTL: Time-to-live in milliseconds for active locks (default:
0- never expires)
Quick Start
Here’s a simple example to get you started:
class MyService {
def lockService
def someMethod() {
def lockKey
try {
lockKey = lockService.acquireLock('/lock/critical-section')
// Perform synchronized operation here
println "Lock acquired, performing critical work..."
} finally {
if (lockKey) {
lockService.releaseLock('/lock/critical-section', [lock: lockKey])
}
}
}
}
Usage
The plugin provides a single non-transactional service (lockService) that handles all lock negotiation. You can inject it into any of your services or controllers.
Basic Lock Operations
Acquiring a Lock
def lockKey = lockService.acquireLock('mylock')
if (lockKey) {
// Lock acquired successfully
// Perform synchronized operation
} else {
println "Unable to obtain lock"
}
Releasing a Lock
try {
def lock = lockService.acquireLock('mylock')
if (lock) {
// DO SYNCHRONIZED WORK
}
} finally {
lockService.releaseLock('mylock', [lock: lock])
}
Lock with Timeout and TTL
def lock = lockService.acquireLock('mylock', [
timeout: 2000l, // Wait up to 2 seconds
ttl: 10000l, // Lock expires after 10 seconds
raiseError: false // Return false instead of throwing exception
])
Domain-based Locking
For convenience, you can lock based on GORM domain instances:
class BookService {
def lockService
def updateBook(Long bookId) {
def book = Book.get(bookId)
def lockKey
try {
lockKey = lockService.acquireLockByDomain(book)
// Update book safely
book.lastModified = new Date()
book.save()
} finally {
if (lockKey) {
lockService.releaseLockByDomain(book, [lock: lockKey])
}
}
}
}
Renewing a Lock
You can extend the lease on an active lock:
def lock = lockService.acquireLock('longRunningTask', [ttl: 30000l])
try {
// Do some work
processPartOne()
// Renew the lock for another 30 seconds
lockService.renewLock('longRunningTask', [lock: lock, ttl: 30000l])
// Continue work
processPartTwo()
} finally {
lockService.releaseLock('longRunningTask', [lock: lock])
}
Querying Active Locks
Get all currently active locks in the system:
Set<String> activeLocks = lockService.getLocks()
activeLocks.each { lockName ->
println "Active lock: ${lockName}"
}
LockService API
Methods
-
acquireLock(String lockName, Map options = null)- Acquire a lock with the given name -
acquireLockByDomain(Object domainInstance, Map options = null)- Acquire a lock for a domain instance -
releaseLock(String lockName, Map options = null)- Release a named lock -
releaseLockByDomain(Object domainInstance, Map options = null)- Release a domain-based lock -
renewLock(String lockName, Map options = null)- Renew an expiring lock -
renewLockByDomain(Object domainInstance, Map options = null)- Renew a domain-based lock -
getLocks()- ReturnsSet<String>of currently active lock names
Options
All methods accept an optional Map parameter with the following options:
-
timeout: Time in milliseconds to wait for the operation to complete (overrides
defaultTimeout) -
ttl: Time in milliseconds for the lock to auto-expire (overrides
defaultTTL) -
raiseError: Boolean to throw exceptions on failure or return boolean status (overrides
raiseError) -
lock: The lock key returned from
acquireLock()(required forreleaseLock()andrenewLock())
Examples
Simple Synchronization
class ReportService {
def lockService
def generateMonthlyReport() {
def lock = lockService.acquireLock('monthly-report', [timeout: 5000l])
if (!lock) {
throw new RuntimeException("Report generation already in progress")
}
try {
// Generate report - only one process will do this at a time
def report = createReport()
saveReport(report)
} finally {
lockService.releaseLock('monthly-report', [lock: lock])
}
}
}
Concurrent Access Control
Using the executor plugin or parallel processing:
import java.util.concurrent.*
class BatchProcessingService {
def lockService
def processItems(List items) {
ExecutorService executor = Executors.newFixedThreadPool(10)
items.each { item ->
executor.submit {
def lock = null
try {
lock = lockService.acquireLock("item-${item.id}", [
timeout: 5000l,
ttl: 60000l
])
if (lock) {
processItem(item)
} else {
log.warn("Could not acquire lock for item ${item.id}")
}
} finally {
if (lock) {
lockService.releaseLock("item-${item.id}", [lock: lock])
}
}
}
}
executor.shutdown()
executor.awaitTermination(1, TimeUnit.HOURS)
}
}
Scheduled Task Coordination
Ensure only one instance runs a scheduled job:
class DataSyncJob {
def lockService
static triggers = {
cron name: 'dataSyncTrigger', cronExpression: '0 0 * * * ?' // Every hour
}
def execute() {
def lock = lockService.acquireLock('data-sync-job', [
timeout: 1000l, // Fail fast if another instance is running
ttl: 3600000l, // Lock expires after 1 hour
raiseError: false
])
if (!lock) {
log.info("Data sync already running on another instance")
return
}
try {
syncDataFromExternalSource()
} finally {
lockService.releaseLock('data-sync-job', [lock: lock])
}
}
}
Advanced Usage
For detailed information on advanced features and patterns, see Advanced Usage.
Topics covered in the advanced guide:
-
Custom Lock Providers
-
Lock Patterns and Best Practices
-
Handling Failures and Timeouts
-
Performance Considerations
-
Testing with Distributed Locks
-
Troubleshooting
Getting Help
-
GitHub Issues: https://github.com/wondrify/distributed-lock
-
Source Code: https://github.com/wondrify/distributed-lock
License
Distributed Lock is open source software licensed under the Apache License 2.0.