Table of Contents

Distributed lock

Overview

Distributed lock is a synchronization primitive that allows only one process to access a shared resource at a time. It is used to implement distributed systems where multiple processes need to access the same resource.

Neos provide a distributed lock implementation that is based on DAPR's distributed lock implementation. It uses a distributed storage system to store the lock information. The lock is acquired by writing a key to the storage system. The key is unique to the lock and the process that acquired it. The lock is released by deleting the key from the storage system.

DAPR implements only a component for distributed lock based on Redis.

Warning

The distributed lock should be used with caution.
This can cause performance issues if used to sequentialize processing.
For example, a correct use of the lock is to prevent the multiple execution of a task by several replicas.

Use a distributed lock

To use a distributed lock, you need to inject the IResourceLock interface into your class. The IResourceLock interface provides methods to acquire and release a lock.

Basic usage

To acquire a lock, call the TryLockAsync method. The method takes the following parameters:

  • lockName (string): A unique key that identifies the lock.
  • owner (string): Indicates the identifier of lock owner, it can be the cluster name.
  • maxLockTimeInSeconds (int): The maximum time in seconds that the lock is held.
  • action (Func<CancellationToken, Task>): The action to perform while the lock is held.
  • cancellationToken (CancellationToken): A token to cancel the operation.
public class MyService
{
    private readonly IResourceLock _resourceLocker;

    public MyService(IResourceLock resourceLock)
    {
        _resourceLocker = resourceLock;
    }

    public async Task DoSomething(CancellationToken cancellationToken)
    {
         ILockResult result = await _resourceLocker.TryLockAsync("UniqueKey", _applicationInfo.ClusterName, 120,
            async (c) =>
            {
                // Do something
            },
            cancellationToken);

        if (result.Success)
        {
            _logger.LogInformation("The operation was executed successfully.");
        }
        else
        {
            _logger.LogWarning("The lock could not be acquired.");
        }
    }
}

When the lock is acquired, the action is executed. If the lock is not acquired, the method returns a result with the Success property set to false. If the lock is acquired, the method returns a result with the Success property set to true and the action is executed. Then the lock is released.

Advanced usage

You can also acquire a lock using the AcquireLockAsync method. The method takes the following parameters:

  • lockName (string): A unique key that identifies the lock.
  • owner (string): Indicates the identifier of lock owner, it can be the cluster name.
  • maxLockTimeInSeconds (int): The maximum time in seconds that the lock is held.
  • cancellationToken (CancellationToken): A token to cancel the lock acquisition.
public class MyService
{
    private readonly IResourceLock _resourceLocker;

    public MyService(IResourceLock resourceLock)
    {
        _resourceLocker = resourceLock;
    }

    public async Task DoSomething(CancellationToken cancellationToken)
    {
        using IAcquireLockResult result = await _resourceLocker.AcquireLockAsync("UniqueKey", _applicationInfo.ClusterName, 120, cancellationToken);

        if (result.Success)
        {
            _logger.LogInformation("The lock was acquired successfully.");
        }
        else
        {
            _logger.LogWarning("The lock could not be acquired.");
        }
    }
}

When the lock is acquired, the lock is held until the lock is released. The lock is released when the using block is exited or when the max lock time is reached.

You can also release a lock using the ReleaseLockAsync method. The method takes the following parameters:

  • lockName (string): A unique key that identifies the lock.
  • owner (string): Indicates the identifier of lock owner.
  • cancellationToken (CancellationToken): A token to cancel the operation.
public class MyService
{
    private readonly IResourceLock _resourceLocker;

    public MyService(IResourceLock resourceLock)
    {
        _resourceLocker = resourceLock;
    }

    public async Task DoSomething(CancellationToken cancellationToken)
    {
        IAcquireLockResult result = await _resourceLocker.AcquireLockAsync("UniqueKey", _applicationInfo.ClusterName, 120, cancellationToken);

        // Do something ...

        await _resourceLocker.ReleaseLockAsync("UniqueKey", _applicationInfo.ClusterName, cancellationToken);
    }
}

WaitForLockAsync method

The WaitForLockAsync method allows you to attempt to acquire a distributed lock by repeatedly trying until the lock is acquired or a specified timeout is reached. This method takes the following parameters:

  • lockName (string): A unique key that identifies the lock.
  • owner (string): The identifier of the lock owner.
  • maxLockTimeInSeconds (int): The maximum time in seconds that the lock is held.
  • action (Func<CancellationToken, Task>): The action to perform while the lock is held.
  • timeoutInMilliseconds (int): The maximum time to wait for the lock acquisition.
  • pollingIntervalInMilliseconds (int, optional): The interval in milliseconds between lock acquisition attempts (default is 100 ms).
  • cancellationToken (CancellationToken): A token to cancel the operation.

This method blocks the current thread until the lock is acquired or the timeout expires. If the lock cannot be acquired within the timeout, the result's Success property will be false.

Warning

Use WaitForLockAsync with caution. Blocking the thread while waiting for a lock can lead to performance issues and thread starvation.
It is recommended to use the retry policy mechanisms provided by DAPR pub/sub instead of blocking the thread with this method. This approach helps avoid unnecessary resource consumption and improves the scalability of your application.

Here is an example of how to use the WaitForLockAsync method to acquire a distributed lock with a timeout and polling interval:

public class MyService
{
    private readonly IResourceLock _resourceLocker;

    public MyService(IResourceLock resourceLock)
    {
        _resourceLocker = resourceLock;
    }

    public async Task DoSomething(CancellationToken cancellationToken)
    {
        ILockResult result = await _resourceLocker.WaitForLockAsync(
            "UniqueKey",
            _applicationInfo.ClusterName,
            120,
            async (c) =>
            {
                // Code to execute while the lock is held
                await Task.Delay(1000, c);
            },
            timeoutInMilliseconds: 10000, // Wait up to 10 seconds to acquire the lock
            pollingIntervalInMilliseconds: 200, // Try every 200 ms
            cancellationToken);

        if (result.Success)
        {
            _logger.LogInformation("The lock was acquired and the action executed.");
        }
        else
        {
            _logger.LogWarning("Failed to acquire the lock within the timeout.");
        }
    }
}

Usage without DAPR on development environment

If you use the distributed lock in a development environment without DAPR, the action is executed without acquiring the lock.