Skip to content

Commit 0979b6c

Browse files
committed
Add Garnet lock feature
1 parent e1b6dfd commit 0979b6c

3 files changed

Lines changed: 269 additions & 0 deletions

File tree

src/OrchardCoreContrib.Garnet/Manifest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@
3838
Dependencies = ["OrchardCoreContrib.Garnet"],
3939
Category = "Distributed Caching"
4040
)]
41+
42+
[assembly: Feature(
43+
Id = "OrchardCoreContrib.Garnet.Lock",
44+
Name = "Garnet Lock",
45+
Description = "Distributed Lock using Garnet.",
46+
Dependencies = ["OrchardCoreContrib.Garnet"],
47+
Category = "Distributed Caching"
48+
)]
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
3+
using OrchardCore.Environment.Shell;
4+
using OrchardCore.Locking;
5+
using OrchardCore.Locking.Distributed;
6+
using StackExchange.Redis;
7+
using System.Diagnostics;
8+
using System.Net;
9+
10+
namespace OrchardCoreContrib.Garnet.Services;
11+
12+
/// <summary>
13+
/// Represents a distributed lock implementation based on Garnet service.
14+
/// </summary>
15+
/// <param name="garnetService">The <see cref="IGarnetService"/>.</param>
16+
/// <param name="garnetOptions">The <see cref="IOptions{GarnetOptions}"/>.</param>
17+
/// <param name="shellSettings">The <see cref="ShellSettings"/>.</param>
18+
/// <param name="logger">The <see cref="ILogger{GarnetLock}"/>.</param>
19+
public class GarnetLock(
20+
IGarnetService garnetService,
21+
IOptions<GarnetOptions> garnetOptions,
22+
ShellSettings shellSettings,
23+
ILogger<GarnetLock> logger) : IDistributedLock
24+
{
25+
private static readonly double _baseDelay = 100;
26+
private static readonly double _maxDelay = 10000;
27+
28+
private readonly GarnetOptions _garnetOptions = garnetOptions.Value;
29+
private readonly string _hostName = Dns.GetHostName() + ':' + Environment.ProcessId;
30+
private readonly string _prefix = garnetService.InstancePrefix + shellSettings.Name + ':';
31+
32+
/// <summary>
33+
/// Waits indefinitely until acquiring a named lock with a given expiration for the current tenant
34+
/// </summary>
35+
/// <param name="key">The key.</param>
36+
/// <param name="expiration">The expiration time for the lock.</param>
37+
public async Task<ILocker> AcquireLockAsync(string key, TimeSpan? expiration = null)
38+
=> (await TryAcquireLockAsync(key, TimeSpan.MaxValue, expiration)).locker;
39+
40+
/// <summary>
41+
/// Tries to acquire a named lock in a given timeout with a given expiration for the current tenant.
42+
/// </summary>
43+
/// <param name="key">The key.</param>
44+
/// <param name="timeout">The timeout for acquiring the lock.</param>
45+
/// <param name="expiration">The expiration time for the lock.</param>
46+
/// <returns></returns>
47+
public async Task<(ILocker locker, bool locked)> TryAcquireLockAsync(string key, TimeSpan timeout, TimeSpan? expiration = null)
48+
{
49+
using (var cts = new CancellationTokenSource(timeout != TimeSpan.MaxValue ? timeout : Timeout.InfiniteTimeSpan))
50+
{
51+
var retries = 0.0;
52+
53+
while (!cts.IsCancellationRequested)
54+
{
55+
var locked = await LockAsync(key, expiration ?? TimeSpan.MaxValue);
56+
57+
if (locked)
58+
{
59+
return (new Locker(this, key), locked);
60+
}
61+
62+
try
63+
{
64+
await Task.Delay(GetDelay(++retries), cts.Token);
65+
}
66+
catch (TaskCanceledException)
67+
{
68+
if (logger.IsEnabled(LogLevel.Debug))
69+
{
70+
logger.LogDebug("Timeout elapsed before acquiring the named lock '{LockName}' after the given timeout of '{Timeout}'.",
71+
_prefix + key, timeout.ToString());
72+
}
73+
}
74+
}
75+
}
76+
77+
return (null, false);
78+
}
79+
80+
public async Task<bool> IsLockAcquiredAsync(string key)
81+
{
82+
if (garnetService.Client == null)
83+
{
84+
await garnetService.ConnectAsync();
85+
86+
if (garnetService.Client == null)
87+
{
88+
logger.LogError("Fails to check whether the named lock '{LockName}' is already acquired.", _prefix + key);
89+
90+
return false;
91+
}
92+
}
93+
94+
try
95+
{
96+
var database = (await ConnectionMultiplexer
97+
.ConnectAsync(GetConfigurationOptions(_garnetOptions)))
98+
.GetDatabase();
99+
100+
return (await database.LockQueryAsync(_prefix + key)).HasValue;
101+
}
102+
catch (Exception e)
103+
{
104+
logger.LogError(e, "Fails to check whether the named lock '{LockName}' is already acquired.", _prefix + key);
105+
}
106+
107+
return false;
108+
}
109+
110+
private async Task<bool> LockAsync(string key, TimeSpan expiry)
111+
{
112+
if (garnetService.Client == null)
113+
{
114+
await garnetService.ConnectAsync();
115+
116+
if (garnetService.Client == null)
117+
{
118+
logger.LogError("Fails to acquire the named lock '{LockName}'.", _prefix + key);
119+
120+
return false;
121+
}
122+
}
123+
124+
try
125+
{
126+
var database = (await ConnectionMultiplexer
127+
.ConnectAsync(GetConfigurationOptions(_garnetOptions)))
128+
.GetDatabase();
129+
130+
return await database.LockTakeAsync(_prefix + key, _hostName, expiry);
131+
}
132+
catch (Exception e)
133+
{
134+
logger.LogError(e, "Fails to acquire the named lock '{LockName}'.", _prefix + key);
135+
}
136+
137+
return false;
138+
}
139+
140+
private async ValueTask ReleaseAsync(string key)
141+
{
142+
try
143+
{
144+
var database = (await ConnectionMultiplexer
145+
.ConnectAsync(GetConfigurationOptions(_garnetOptions)))
146+
.GetDatabase();
147+
148+
await database.LockReleaseAsync(_prefix + key, _hostName);
149+
}
150+
catch (Exception e)
151+
{
152+
logger.LogError(e, "Fails to release the named lock '{LockName}'.", _prefix + key);
153+
}
154+
}
155+
156+
private void Release(string key)
157+
{
158+
try
159+
{
160+
var database = ConnectionMultiplexer
161+
.ConnectAsync(GetConfigurationOptions(_garnetOptions))
162+
.GetAwaiter()
163+
.GetResult()
164+
.GetDatabase();
165+
166+
database.LockRelease(_prefix + key, _hostName);
167+
}
168+
catch (Exception e)
169+
{
170+
logger.LogError(e, "Fails to release the named lock '{LockName}'.", _prefix + key);
171+
}
172+
}
173+
174+
private sealed class Locker(GarnetLock garnetLock, string key) : ILocker
175+
{
176+
private bool _disposed;
177+
178+
public ValueTask DisposeAsync()
179+
{
180+
if (_disposed)
181+
{
182+
return default;
183+
}
184+
185+
_disposed = true;
186+
187+
return garnetLock.ReleaseAsync(key);
188+
}
189+
190+
public void Dispose()
191+
{
192+
if (_disposed)
193+
{
194+
return;
195+
}
196+
197+
_disposed = true;
198+
199+
garnetLock.Release(key);
200+
}
201+
}
202+
203+
private static TimeSpan GetDelay(double retries)
204+
{
205+
var delay = _baseDelay * (1.0 + ((Math.Pow(1.8, retries - 1.0) - 1.0) * (0.6 + new Random().NextDouble() * 0.4)));
206+
207+
return TimeSpan.FromMilliseconds(Math.Min(delay, _maxDelay));
208+
}
209+
210+
// TODO: Use explicit conversion operators to convert between GarnetOptions and ConfigurationOptions
211+
private static ConfigurationOptions GetConfigurationOptions(GarnetOptions garnetOptions)
212+
{
213+
var endPoints = new EndPointCollection
214+
{
215+
new DnsEndPoint(garnetOptions.Host, garnetOptions.Port)
216+
};
217+
var configOptions = new ConfigurationOptions
218+
{
219+
EndPoints = endPoints,
220+
ConnectTimeout = (int)TimeSpan.FromSeconds(2).TotalMilliseconds,
221+
SyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
222+
AsyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
223+
ReconnectRetryPolicy = new LinearRetry((int)TimeSpan.FromSeconds(10).TotalMilliseconds),
224+
ConnectRetry = 5,
225+
IncludeDetailInExceptions = true,
226+
AbortOnConnectFail = true,
227+
User = garnetOptions.UserName,
228+
Password = garnetOptions.Password
229+
};
230+
231+
if (Debugger.IsAttached)
232+
{
233+
configOptions.SyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
234+
configOptions.AsyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
235+
}
236+
237+
return configOptions;
238+
}
239+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using OrchardCore.Locking.Distributed;
3+
using OrchardCore.Modules;
4+
using OrchardCoreContrib.Garnet.Services;
5+
6+
namespace OrchardCoreContrib.Garnet;
7+
8+
/// <summary>
9+
/// Represensts a startup point to register the required services by Garnet lock feature.
10+
/// </summary>
11+
[Feature("OrchardCoreContrib.Garnet.Lock")]
12+
public class GarnetLockStartup : StartupBase
13+
{
14+
/// <inheritdoc/>
15+
public override void ConfigureServices(IServiceCollection services)
16+
{
17+
if (services.Any(d => d.ServiceType == typeof(IGarnetService)))
18+
{
19+
services.AddSingleton<IDistributedLock, GarnetLock>();
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)