Grails plugin to provide a distributed locking framework and interface underlying providers
Groovy Shell Batchfile

README.md

Grails Distributed Lock

This plugin provides a framework and interface for a synchronization mechanism that can be distributed outside the context of the app container it runs in. 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). This plugin aims to make that easier by providing a simple service to facilitate this, as well as defining an interface for adding low level providers.

In the current release, only a provider for redis currently exists, which depends on the grails-redis plugin. Any other providers are welcome contributions.

Release Notes

  • 0.1: Initial release
  • 0.2: Adding withLock() variations for convenience to LockService impl
  • 0.2.1: Minor bug fixes in configuration
  • 0.2.2: Minor fix for domain class identification

Things to be Done

Installation

Add the plugin to your BuildConfig.groovy:

plugins {
	compile ":distributed-lock:0.1.0"
}

Configuration

First thing is to configure your redis store. Add sample config to your Config.groovy:

grails {
	redis {
		host = 'localhost'
		port = 6379
	}
}

NOTE: Please see grails-redis for more configuration details for your redis store

Next, configure your distributed-lock options:

distributedLock {
	provider {
		type = RedisLockProvider // Currently the only available provider
		// NOTE: Use only if not using the default redis connection
		// connection = 'otherThanDefault'
	}
	raiseError = true //optional
	defaultTimeout = 10000l //optional
	defaultTTL = 1000l * 60l * 60l //optional (1 hour)
	namespace = 'example-app' //optional
}

Configuration options:

  • provider: This block is used to describe your implementation provider

    • type: The implementation class of a low level provider (currently only RedisLockProvider avail)
    • connection: Used by redis provider to specify specific connection (if using grails-redis multi connection)
  • raiseError: Config option to throw exceptions on failures as opposed to just returning boolean status (defaults to 'true')

  • namespace: Specify a namespace for your lock keys (defaults to 'distributed-lock')

  • defaultTimeout: The default time (in millis) to wait for a lock acquire before failing (defaults to '30000')

  • defaultTTL: The TTL (in millis) for an active lock. If its not released in this much time, it will be force released when expired. Value defaults to 0 (never expires)

Usage

The plugin provides a single non transactional service that handles all lock negotiation that you can inject in any of your services

class MyService {
	def lockService
	
	def someMethod() {
		def lockKey = lockService.acquireLock('/lock/a/shared/fs')
	}
}

LockService Methods

  • acquireLock( String lockName, Map options = null ): attempts to acquire a lock of lockName with the optional options. Returns true on success.
  • acquireLockByDomain( Object domainInstance, Map options = null ): Convenience method to acquire a lock derived from a domain class instance. Returns true on success..
  • releaseLock( String lockName, Map options = null ): release a lock lockName when no longer needed. Returns true on success.
  • releaseLockByDomain( Object domainInstance, Map options = null ): release a lock derived from a domain class instance. Returns true on success.
  • renewLock( String lockName, Map options = null ): Can renew the lease on an expiring active lock. If no ttl specified in options, lock ceases to become volatile. Returns true on success.
  • renewLockByDomain( Object domainInstance, Map options = null_ ): Renew a lock derived from a domain class instance. Returns true on success.
  • getLocks(): Returns a Set<String> of lock names that are currently active in the system.

Options

The optional Map allows you to override certain configuration settings just for the context of your method call. Options include:

  • timeout: time in millis to wait for for the operation to complete before returning failure
  • ttl: time in millis for an acquired lock to expire itself if not released
  • raiseError: boolean instructing whether to throw an exception on failure or just return boolean status

Examples

Simple usages of LockService:

def lockKey = lockService.acquireLock('mylock')
	
if (lockKey) {
	// Perform on operation we want synchronized
}
else {
	println("Unable to obtain lock")
}
	
// try/finally to release lock
try {
	def lock = lockService.acquireLock('lock2', [timeout:2000l, ttl:10000l, raiseError:false])
	if (lock) {
		// DO SOME SYNCHRONIZED STUFF
	}
}
finally {
	lockService.releaseLock('lock2',[lock: lock])
}

Threaded sample using executor plugin:

def lockService
	
(0..10).each { i ->
	runAsync {
		def lock
		try {
			if (lock = lockService.acquireLock('test-run', [timeout:5000l]))
				println("Lock acquired for thread ${i}")
			else
				println("Failed to acquire lock for thread ${i}")
				
			// Sleep for random amount of time
			sleep(new Random().nextInt(1000) as Long)
		}
		finally {
			lockService.releaseLock('test-run',[lock:lock])
		}
	}
}

Extending/Contributing

To add additional providers is simple. Simply extend the abstract com.bertram.lock.LockProvider class and implement its abstract methods. Once the new provider is implemented, it must be added to the LockServiceConfigurer configuration method. Please submit contributions via pull request.