Show / Hide Table of Contents

How to develop your own lock manager

A lock manager must implement the ILockManager interface, but for your own sanity, you should derive your implementation from LockManagerBase.

The LockManagerBase class already implements the core functionality and uses a notion of a transaction that must be used to store and retrieve the active locks. No lock can be modified until the transaction was either committed or rolled back (by disposing without committing).

For this guide, we implement a lock manager that stores the active locks in files in a special directory.

You can find the repository for this project on GitHub.

Create a new class

First, you have to create a new class derived from LockManagerBase. It needs to implement the BeginTransactionAsync method.

We don't have a database that supports transactions, so we have to use a semaphore to restrict concurrent access to the file system. Another solution might be using a lock file, which is overkill for this example.

Storage

The file used to store the locks should be configured using an options class. This class should implement the ILockManagerOptions interface to avoid multiple options classes for the lock manager. A sufficient way to initialize the Rounding property is to create a new instance of DefaultLockTimeRounding with DefaultLockTimeRoundingMode.OneSecond as constructor parameter.

Constructor

The constructor should take a parameter of type IOptions<TextFileLockManagerOptions> and we pass its Value to the base class.

Synchronization

For the synchronization, we use a SemaphoreSlim and create it with an initial count of 1. In the BeginTransactionAsync method, we first have to call SemaphoreSlim.WaitAsync and then we have to call Release on the semaphore in the Dispose method of our ILockManagerTransaction implementation. For a cluster of WebDAV servers using the same lock file, one may use a lock file to synchronize access.

You can find the changes in this GIT commit.

File format

We just use a simple JSON file because we usually don't have many active locks and reading and writing a whole file doesn't cause a huge performance penalty.

Structure

The structure of the file is just a list of objects that implement the IActiveLock interface, but it also has to implement every property with a setter and an additional Owner property.

Load locks after restart

We have to load all active locks when we first open a transaction. Those locks must be passed to the lock cleanup task to ensure that the locks will be released when they expire. The implementation can be found in this GIT commit.

Implement the transaction interface

Now we need to implement the transaction interface LockManagerBase.ILockManagerTransaction.

In the new transaction class, we load the JSON file during construction and save the JSON file in the ILockManagerTransaction.CommitAsync method.

The lock needs to be converted to an internal representation using the AutoMapper.

Transaction interface methods

The transaction interface consists of the following parts:

  • ILockManagerTransaction.GetActiveLocksAsync

    This function is used to get all active locks. We just cast the values of the dicitonary to an IActiveLock and return those as a list. A sample implementation can be found in this GIT commit.

  • ILockManagerTransaction.AddAsync

    Adds a new active lock. We're just adding the lock to the dictionary. An example implementation (with tests) can be found on GitHub.

  • ILockManagerTransaction.UpdateAsync

    Updates an active lock. This is done when the lock was refreshed. The implementation for this example can be found on GitHub.

  • ILockManagerTransaction.RemoveAsync

    Removes an active lock.

  • ILockManagerTransaction.GetAsync

    Gets an active lock by its state token.

  • ILockManagerTransaction.CommitAsync

    Commits all changes made during the transaction. In our implementation, we'll just save the locks as JSON file.

The implementation for RemoveAsync and GetAsync can be found in this GitHub commit.

GetActiveLocksAsync

This function just returns every row in the table holding the active locks.

IDisposable implementation

This interface also inherits from IDisposable. It depends on the state of the transaction what happens when the Dispose function is called:

  • CommitAsync called before Dispose results in a disposable of the resources.
  • Dispose without a CommitAsync results in a rollback and a disposable of the resources.

Additional information

All paths are the paths as seen by the client. When multiple different paths point to the same file system location (through mounts or symbolic links), then the client should override the LockManagerBase.NormalizePath function to provide a normalized path that represents a global path that is the same across different clients.

Summary

The easiest way to implement a lock manager is to use a database that already supports transactions, but almost everything can be used to store the transactions. The most important thing is the synchronized access to the locks. It is also very important, that the BeginTransaction method blocks the caller until it's safe to update the locks.

For databases like MongoDB, a two-phase commit is encouraged.

  • Improve this Doc
Back to top Copyright © 2016-2017 Fubar Development Junker
Generated by DocFX