Skip to content

Commit

Permalink
Merge pull request #18 from riberk/#8_docs
Browse files Browse the repository at this point in the history
#8 Documentation and api small changes
  • Loading branch information
riberk committed Jul 20, 2020
2 parents 7197237 + 0e35e6d commit e43655d
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 42 deletions.
134 changes: 132 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,132 @@
# redlock
Distributed locks with Redlock algorithm
# RedlockDotNet

Distributed locks with [Redlock](https://redis.io/topics/distlock) algorithm for .net projects


## Simple usage with Microsoft dependency injection


```csharp

IServiceCollection services = new ServiceCollection();
services.AddRedlock().AddRedisStorage(b => {
b.AddInstance("redis1:6379");
b.AddInstance("redis2:6379");
b.AddInstance("redis3:6379");
b.AddInstance("redis4:6379");
b.AddInstance("redis5:6379");
});

```

Then you can inject singleton service `IRedlockFactory` and use it

```csharp
IRedlockFactory lockFactory;
// if operation failed in 3 repeats (default behavior), an exception will be thrown
using (var redlock = lockFactory.Create("locking-resource", TimeSpan.FromSeconds(30)))
{
// this we got the lock
}
// lock is automaticaly released on dispose
```

All methods has async overloads
```csharp
await using var redlock = await lockFactory.CreateAsync("resource", TimeSpan.FromSeconds(30));
```

`IRedlockFactory` has many extensions and overloads, you can change default behavior, for example:

```csharp
// use default ttl - 30s
var redlock = lockFactory.Create("resource");

// Try lock 'resource' while cancellationToken is not cancelled.
// Waits random (but no more than 200ms) interval between repeats.
// If cancellation was requested, an exception will be thrown
var redlock = lockFactory.Create("resource", TimeSpan.FromSeconds(10), new CancellationRedlockRepeater(cancellationToken), maxWaitMs: 200);

```

AddInstance has overloads and you can configure redis store options

```csharp

IServiceCollection services = new ServiceCollection();
services.AddRedlock().AddRedisStorage(b => {

// connection string is StackExchange.Redis compatible
// https://stackexchange.github.io/StackExchange.Redis/Configuration
b.AddInstance("redis1:6379");

// use database 5 on redis server and set name 'second redis server' for logs
b.AddInstance("redis2:6379", database: 5, name: "second redis server");

// use ConfigurationOptions for configure
var conf = new ConfigurationOptions
{
EndPoints =
{
IPEndPoint.Parse("127.0.0.1:6379")
}
};
b.AddInstance(conf);
}, opt =>
{
// Configure clock drift factor for increase or decrease min validity
opt.ClockDriftFactor = 0.3f;

// Change redis key naming policy
opt.RedisKeyFromResourceName = resource => $"locks_{resource}";
});

```

## Use algorithm without di with other lock stores

All the functionality of the algorithm is in the static methods of `Redlock` struct. For use it, you need implements interface `IRedlockImplementation` and `IRedlockInstance`

`IRedlockInstance` represents instance to store distributed lock (e.g. one independent redis server). It contains methods for locking and unlocking a specific resource with a specific nonce for a specific time

`IRedlockImplementation` is a simple container for the `IRedlockInstance`s array and the `MinValidity' method, which calculates the minimum time that the lock will take

```csharp
IRedlockImplementation impl;
ILogger log;

// Try lock "resource" with automatic unblocking after 10 seconds
Redlock? redlock = Redlock.TryLock("resource", "nonce", TimeSpan.FromSeconds(10), impl, log);

Redlock? redlock = Redlock.TryLock("resource", "nonce", TimeSpan.FromSeconds(10), impl, log, new CancellationRedlockRepeater(cancellationToken), maxWaitMs: 200);

// lock or exception
Redlock redlock = Redlock.Lock("resource", "nonce", TimeSpan.FromSeconds(10), impl, log, new CancellationRedlockRepeater(cancellationToken), maxWaitMs: 200);

```

### Repeaters

Repeater is a way for separate the algorithm from the logic of repetion. It`s a simple interface
```csharp
public interface IRedlockRepeater
{
bool Next();

// Has default interface implementation Thread.Sleap(random(0,maxWaitMs))
void WaitRandom(int maxWaitMs);

// Has default interface implementation Task.Delay(random(0,maxWaitMs))
ValueTask WaitRandomAsync(int maxWaitMs, CancellationToken cancellationToken = default);

// Has default interface implementation RedlockException
Exception CreateException(string resource, string nonce, int attemptCount);
}
```

We have three repeater implementation:
* `CancellationRedlockRepeater` - repeat while CancellationToken does not canceled
* `MaxRetriesRedlockRepeater` - repeat max count
* `NoopRedlockRepeater` - no repeat


Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public void MinValidity_ClockDriftFactor()
var minValidity = _impl.MinValidity(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1));
Assert.Equal(TimeSpan.FromMilliseconds(8898), minValidity);
}

[Fact]
public void MinValidity_ClockDriftFactor03()
{
_opt.ClockDriftFactor = 0.5f;
var minValidity = _impl.MinValidity(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1));
Assert.Equal(TimeSpan.FromMilliseconds(3998), minValidity);
}

[Fact]
public void Instances()
Expand Down
2 changes: 1 addition & 1 deletion src/RedlockDotNet.Redis.Tests/RedisRedlockInstanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public RedisRedlockInstanceTests(RedisFixture redis, ITestOutputHelper console)
{
_console = console;
_logger = new MemoryLogger();
_instance = new RedisRedlockInstance(Db, "i", _logger);
_instance = new RedisRedlockInstance(Db, s => s, "i", _logger);
}

[Fact]
Expand Down
25 changes: 12 additions & 13 deletions src/RedlockDotNet.Redis.Tests/RedisRedlockIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ namespace RedlockDotNet.Redis.Tests
{
public class RedisRedlockIntegrationTests : RedisTestBase, IDisposable
{
private readonly RedisRedlockOptions _opt;
private readonly RedisRedlockImplementation _5Inst;
private readonly MemoryLogger _log;
private readonly RedisRedlockImplementation _noQuorum;
Expand All @@ -22,23 +21,23 @@ public class RedisRedlockIntegrationTests : RedisTestBase, IDisposable
public RedisRedlockIntegrationTests(RedisFixture redis, ITestOutputHelper console) : base(redis)
{
_console = console;
_opt = new RedisRedlockOptions();
var opt = new RedisRedlockOptions();
_log = new MemoryLogger();
_5Inst = new RedisRedlockImplementation(new []
{
new RedisRedlockInstance(() => Redis.Redis1.GetDatabase(), "1", _log),
new RedisRedlockInstance(() => Redis.Redis2.GetDatabase(), "2", _log),
new RedisRedlockInstance(() => Redis.Redis3.GetDatabase(), "3", _log),
new RedisRedlockInstance(() => Redis.Redis4.GetDatabase(), "4", _log),
new RedisRedlockInstance(() => Redis.Redis5.GetDatabase(), "5", _log),
}, Options.Create(_opt));
new RedisRedlockInstance(() => Redis.Redis1.GetDatabase(), s => s, "1", _log),
new RedisRedlockInstance(() => Redis.Redis2.GetDatabase(), s => s, "2", _log),
new RedisRedlockInstance(() => Redis.Redis3.GetDatabase(), s => s, "3", _log),
new RedisRedlockInstance(() => Redis.Redis4.GetDatabase(), s => s, "4", _log),
new RedisRedlockInstance(() => Redis.Redis5.GetDatabase(), s => s, "5", _log),
}, Options.Create(opt));
_noQuorum = new RedisRedlockImplementation(new []
{
new RedisRedlockInstance(() => Redis.Redis1.GetDatabase(), "1", _log),
new RedisRedlockInstance(() => Redis.Redis2.GetDatabase(), "2", _log),
new RedisRedlockInstance(() => Redis.Unreachable1.GetDatabase(), "u1", _log),
new RedisRedlockInstance(() => Redis.Unreachable2.GetDatabase(), "u2", _log),
}, Options.Create(_opt));
new RedisRedlockInstance(() => Redis.Redis1.GetDatabase(), s => s, "1", _log),
new RedisRedlockInstance(() => Redis.Redis2.GetDatabase(), s => s, "2", _log),
new RedisRedlockInstance(() => Redis.Unreachable1.GetDatabase(), s => s, "u1", _log),
new RedisRedlockInstance(() => Redis.Unreachable2.GetDatabase(), s => s, "u2", _log),
}, Options.Create(opt));
}

[Fact]
Expand Down
16 changes: 10 additions & 6 deletions src/RedlockDotNet.Redis.Tests/RedlockDiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public RedlockDiTests(RedisFixture fixture) : base(fixture)
b.AddInstance(TestConfig.Instance.GetConnectionString("redis1"), "1");
b.AddInstance(TestConfig.Instance.GetConnectionString("redis2"), "2");
b.AddInstance(TestConfig.Instance.GetConnectionString("redis3"), "3");
}, opt =>
{
opt.ClockDriftFactor = 0.3f;
opt.RedisKeyFromResourceName = resource => $"locks_{resource}";
});
}

Expand All @@ -49,14 +53,14 @@ public void GetFromDi_ThenLock_ThenUnlock()
var f = _services.BuildServiceProvider().GetRequiredService<IRedlockFactory>();
using (var l = f.Create("r"))
{
Assert.Equal(l.Nonce, Redis.Redis1.GetDatabase().StringGet("r"));
Assert.Equal(l.Nonce, Redis.Redis2.GetDatabase().StringGet("r"));
Assert.Equal(l.Nonce, Redis.Redis3.GetDatabase().StringGet("r"));
Assert.Equal(l.Nonce, Redis.Redis1.GetDatabase().StringGet("locks_r"));
Assert.Equal(l.Nonce, Redis.Redis2.GetDatabase().StringGet("locks_r"));
Assert.Equal(l.Nonce, Redis.Redis3.GetDatabase().StringGet("locks_r"));
}

Assert.False(Redis.Redis1.GetDatabase().KeyExists("r"));
Assert.False(Redis.Redis2.GetDatabase().KeyExists("r"));
Assert.False(Redis.Redis3.GetDatabase().KeyExists("r"));
Assert.False(Redis.Redis1.GetDatabase().KeyExists("locks_r"));
Assert.False(Redis.Redis2.GetDatabase().KeyExists("locks_r"));
Assert.False(Redis.Redis3.GetDatabase().KeyExists("locks_r"));
}

[Fact]
Expand Down
40 changes: 32 additions & 8 deletions src/RedlockDotNet.Redis/RedisRedlockInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace RedlockDotNet.Redis
public sealed class RedisRedlockInstance : IRedlockInstance
{
internal readonly Func<IDatabase> SelectDb;
private readonly Func<string, string> _createKeyFromResourceName;
private readonly string _name;
private readonly ILogger _logger;

Expand All @@ -29,11 +30,13 @@ public sealed class RedisRedlockInstance : IRedlockInstance
/// </summary>
public RedisRedlockInstance(
Func<IDatabase> selectDb,
Func<string, string> createKeyFromResourceName,
string name,
ILogger logger
)
{
SelectDb = selectDb;
_createKeyFromResourceName = createKeyFromResourceName;
_name = name;
_logger = logger;
}
Expand Down Expand Up @@ -79,7 +82,7 @@ public async Task UnlockAsync(string resource, string nonce)
/// <summary>
/// Build key for redis from resource name
/// </summary>
private string Key(string resource) => resource;
private string Key(string resource) => _createKeyFromResourceName(resource);

/// <inheritdoc />
public override string ToString() => _name;
Expand All @@ -88,12 +91,18 @@ public async Task UnlockAsync(string resource, string nonce)
/// Create <see cref="RedisRedlockInstance"/> from <see cref="ConnectionMultiplexer"/>
/// </summary>
/// <param name="con"></param>
/// <param name="createKeyFromResourceName"></param>
/// <param name="database">Number of the database where the locks will be stored</param>
/// <param name="name">Instance name for logs and ToString</param>
/// <param name="logger"></param>
/// <returns></returns>
public static IRedlockInstance Create(IConnectionMultiplexer con, int database, string name, ILogger logger)
=> new RedisRedlockInstance(() => con.GetDatabase(database), name, logger);
public static IRedlockInstance Create(
IConnectionMultiplexer con,
Func<string, string> createKeyFromResourceName,
int database,
string name,
ILogger logger
) => new RedisRedlockInstance(() => con.GetDatabase(database), createKeyFromResourceName, name, logger);

/// <summary>
/// Create <see cref="RedisRedlockInstance"/> from <see cref="ConnectionMultiplexer"/>
Expand All @@ -102,11 +111,16 @@ public static IRedlockInstance Create(IConnectionMultiplexer con, int database,
/// Name of instance sets to first connection endpoint ToString
/// </remarks>
/// <param name="con"></param>
/// <param name="createKeyFromResourceName"></param>
/// <param name="database">Number of the database where the locks will be stored</param>
/// <param name="logger"></param>
/// <returns></returns>
public static IRedlockInstance Create(IConnectionMultiplexer con, int database, ILogger logger)
=> Create(con, database, GetName(con), logger);
public static IRedlockInstance Create(
IConnectionMultiplexer con,
Func<string, string> createKeyFromResourceName,
int database,
ILogger logger
) => Create(con, createKeyFromResourceName, database, GetName(con), logger);

/// <summary>
/// Create <see cref="RedisRedlockInstance"/> from <see cref="ConnectionMultiplexer"/>
Expand All @@ -115,11 +129,16 @@ public static IRedlockInstance Create(IConnectionMultiplexer con, int database,
/// Database sets to default (<see cref="ConnectionMultiplexer.GetDatabase"/>)
/// </remarks>
/// <param name="con"></param>
/// <param name="createKeyFromResourceName"></param>
/// <param name="name">Instance name for logs and ToString</param>
/// <param name="logger"></param>
/// <returns></returns>
public static IRedlockInstance Create(IConnectionMultiplexer con, string name, ILogger logger)
=> new RedisRedlockInstance(() => con.GetDatabase(), name, logger);
public static IRedlockInstance Create(
IConnectionMultiplexer con,
Func<string, string> createKeyFromResourceName,
string name,
ILogger logger
) => new RedisRedlockInstance(() => con.GetDatabase(), createKeyFromResourceName, name, logger);

/// <summary>
/// Create <see cref="RedisRedlockInstance"/> from <see cref="ConnectionMultiplexer"/>
Expand All @@ -131,9 +150,14 @@ public static IRedlockInstance Create(IConnectionMultiplexer con, string name, I
/// Database sets to default (<see cref="ConnectionMultiplexer.GetDatabase"/>)
/// </remarks>
/// <param name="con"></param>
/// <param name="createKeyFromResourceName"></param>
/// <param name="logger"></param>
/// <returns></returns>
public static IRedlockInstance Create(IConnectionMultiplexer con, ILogger logger) => Create(con, GetName(con), logger);
public static IRedlockInstance Create(
IConnectionMultiplexer con,
Func<string, string> createKeyFromResourceName,
ILogger logger
) => Create(con, createKeyFromResourceName, GetName(con), logger);

private static string GetName(IConnectionMultiplexer con) => con.GetEndPoints().FirstOrDefault()?.ToString() ?? "NO_ENDPOINT";
}
Expand Down
5 changes: 5 additions & 0 deletions src/RedlockDotNet.Redis/RedisRedlockOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;

namespace RedlockDotNet.Redis
{
/// <summary>
Expand All @@ -9,5 +11,8 @@ public class RedisRedlockOptions
/// Drift factor for system clock (multiply with ttl of lock)
/// </summary>
public float ClockDriftFactor { get; set; } = 0.01f;

/// <summary>Creates redis key from name of locking resource</summary>
public Func<string, string> RedisKeyFromResourceName { get; set; } = k => k;
}
}
Loading

0 comments on commit e43655d

Please sign in to comment.