Skip to content

claytonandersen/DistributedLock

 
 

Repository files navigation

DistributedLock

DistributedLock is a lightweight .NET library that makes it easy to set up and use system-wide or fully-distributed locks.

DistributedLock is available for download as a NuGet package. NuGet Status

Release notes

Features

System-wide locks

System-wide locks are great for synchronizing between processes or .NET application domains:

var myLock = new SystemDistributedLock("SystemLock");

using (myLock.Acquire())
{
	// this block of code is protected by the lock!
}

Fully-distributed locks

DistributedLock allows you to easily leverage MSFT SQLServer's application lock functionality to provide synchronization between machines in a distributed environment. To use this functionality, you'll need a SQLServer connection string:

var connectionString = ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString;
var myLock = new SqlDistributedLock("SqlLock", connectionString);

using (myLock.Acquire())
{
	// this block of code is protected by the lock!
}

As of version 1.1.0, SqlDistributedLocks can now be scoped to existing IDbTransaction and/or IDbConnection objects as an alternative to passing a connection string directly (in which case the lock manages its own connection).

Semaphores

DistributedLock contains an implementation of a distributed semaphore with an API similar to the framework's non-distributed SemaphoreSlim class. Since the implementation is based on SQLServer application locks, this can be used to synchronize across different machines.

The semaphore acts like a lock that can be acquired by a fixed number of processes/threads simultaneously instead of a single process/thread. This capability is frequently used to "throttle" access to some resource such as a database or email server, generally with the goal of preventing it from becoming overloaded. In such cases, a classic mutex lock is inappropriate because we do want to allow concurrent access and simply want to cap the level of concurrency. For example:

var semaphore = new SqlDistributedSemaphore("ComputeDatabase", 5, connectionString);
using (semaphore.Acquire())
{
	// only 5 callers can be inside this block concurrently
	UseComputeDatabase();
}

Reader-writer locks

DistributedLock contains an implementation of a distributed reader-writer lock with an API similar to the framework's non-distributed ReaderWriterLockSlim class. Since the implementation is based on SQLServer application locks, this can be used to synchronize across different machines.

The reader-writer lock allows for multiple readers or one writer. Furthermore, at most one reader can be in upgradeable read mode, which allows for upgrading from read mode to write mode without relinquishing the read lock. Here's an example showing how a reader-writer lock could synchronize access to a distributed cache:

class DistributedCache
{
	private readonly SqlDistributedReaderWriterLock cacheLock = 
		new SqlDistributedReaderWriterLock("DistributedCache", connectionString);
		
	public string Get(string key)
	{
		using (this.cacheLock.AcquireReadLock())
		{
			return /* read from cache */
		}
	}
	
	public void Add(string key, string value)
	{
		using (this.cacheLock.AcquireWriteLock())
		{
			/* write to cache */
		}
	}
	
	public void AddOrUpdate(string key, string value)
	{
		using (var upgradeableHandle = this.cache.AcquireUpgradeableReadLock())
		{
			if (/* read from cache */) { return; }
			
			upgradeableHandle.UpgradeToWriteLock();
			
			/* write to cache */
		}
	}
}

Naming locks

For all types of locks, the name of the lock defines its identity within its scope. While in general most names will work, the names are ultimately constrained by the underlying technologies used for locking. If you don't want to worry (particularly if when generating names dynamically), you can use the GetSafeLockName method each lock type to convert an arbitrary string into a consistent valid lock name:

string baseName = // arbitrary logic
var lockName = SqlDistributedLock.GetSafeLockName(baseName);
var myLock = new SqlDistributedLock(lockName);

Other features

TryLock

All locks support a "try" mechanism so that you can attempt to claim the lock without committing to it:

using (var handle = myLock.TryAcquire())
{
	if (handle != null)
	{
		// I have the lock!
	}
	else
	{
		// someone else has it!
	}
}

Async

All locks support async acquisition so that you don't need to consume threads waiting for locks to become available:

using (await myLock.AcquireAsync())
{
	// this block of code is protected by the lock!
	
	// locks can be used to protect async code
	await webClient.DownloadStringAsync(...);
}

Note that because of this locks do not have thread affinity unlike the Monitor and Mutex .NET synchronization classes and are not re-entrant.

Timeouts

All lock methods support specifying timeouts after which point TryAcquire calls will return null and Acquire calls will throw a TimeoutException:

// wait up to 5 seconds to acquire the lock
using (var handle = myLock.TryAcquire(TimeSpan.FromSeconds(5)))
{
	if (handle != null)
	{
		// I have the lock!
	}
	else
	{
		// timed out waiting for someone else to give it up
	}
}

Cancellation

All lock methods support passing a CancellationToken which, if triggered, will break out of the wait:

// acquire the lock, unless someone cancels us
CancellationToken token = ...
using (myLock.Acquire(cancellationToken: token))
{
	// this block of code is protected by the lock!
}

Connection management

When using SQL-based locks, DistributedLock exposes several options for managing the underlying connection/transaction that scopes the lock:

  • Explicit: you can pass in the IDbConnection/IDbTransaction instance that provides lock scope. This is useful when you don't have access to a connection string or when you want the locking to be tied closely to other SQL operations being performed.
  • Connection: the lock internally manages a SqlConnection instance. The lock is released by calling sp_releaseapplock after which the connection is disposed. This is the default mode.
  • Transaction: the lock internally manages a SqlTransaction instance. The lock is released by disposing the transaction.
  • Connection Multiplexing: the library internally manages a pool of SqlConnection instances, each of which may be used to hold multiple locks simultaneously. This is particularly helpful for high-load scenarios since it can drastically reduce load on the underlying connection pool.
  • Azure: similar to the "Connection" strategy, but also automatically issues periodic background queries on the underlying connection to keep it from looking idle to the Azure connection governor. See #5 for more details.

Most of the time, you'll want to use the default connection strategy. See more details about the various strategies here.

Release notes

  • 1.5.0
    • Added cross-platform support via Microsoft.Data.SqlClient (#25). This feature is available for .NET Standard >= 2.0. Thanks to @alesebi91 for helping with the implementation and testing!
    • Added C#8 nullable annotations (#31)
    • Fixed minor bug in connection multiplexing which could lead to more lock contention (#32)
  • 1.4.0
    • Added a SQL-based distributed semaphore (#7)
    • Fix bug where SqlDistributedLockConnectionStrategy.Azure would leak connections, relying on GC to reclaim them (#14). Thanks zavalita1 for investigating this issue!
    • Throw a specific exception type (DeadlockException) rather than the generic InvalidOperationException when a deadlock is detected (#11)
  • 1.3.1 Minor fix to avoid "leaking" isolation level changes in transaction-based locks (#8). Also switched to the VS2017 project file format
  • 1.3.0 Added an Azure connection strategy to keep lock connections from becoming idle and being reclaimed by Azure's connection governor (#5)
  • 1.2.0
    • Added a SQL-based distributed reader-writer lock
    • .NET Core support via .NET Standard
    • Changed the default locking scope for SQL distributed lock to be a connection rather than a transaction, avoiding cases where long-running transactions can block backups
    • Allowed for customization of the SQL distributed lock connection strategy when connecting via a connection string
    • Added a new connection strategy which allows for multiplexing multiple held locks onto one connection
    • Added IDbConnection/IDbTransaction constructors (#3)
  • 1.1.0 Added support for SQL distributed locks scoped to existing connections/transactions
  • 1.0.1 Minor fix when using infinite timeouts
  • 1.0.0 Initial release

About

A .NET library for distributed synchronization

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 100.0%