Skip to content

Commit f22544b

Browse files
committed
Merge branch 'garnet-lock'
# Conflicts: # src/OrchardCoreContrib.Modules.Web/appsettings.json
2 parents 2452d35 + 23dac70 commit f22544b

7 files changed

Lines changed: 335 additions & 38 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using StackExchange.Redis;
2+
using System.Diagnostics;
3+
using System.Net;
4+
5+
namespace OrchardCoreContrib.Garnet;
6+
7+
internal class GarnetOptionsConverter
8+
{
9+
public static ConfigurationOptions ConvertToConfigurationOptions(GarnetOptions garnetOptions)
10+
{
11+
var endPoints = new EndPointCollection
12+
{
13+
new DnsEndPoint(garnetOptions.Host, garnetOptions.Port)
14+
};
15+
var configurationOptions = new ConfigurationOptions
16+
{
17+
EndPoints = endPoints,
18+
ConnectTimeout = (int)TimeSpan.FromSeconds(2).TotalMilliseconds,
19+
SyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
20+
AsyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
21+
ReconnectRetryPolicy = new LinearRetry((int)TimeSpan.FromSeconds(10).TotalMilliseconds),
22+
ConnectRetry = 5,
23+
IncludeDetailInExceptions = true,
24+
AbortOnConnectFail = true,
25+
User = garnetOptions.UserName,
26+
Password = garnetOptions.Password
27+
};
28+
29+
if (Debugger.IsAttached)
30+
{
31+
configurationOptions.SyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
32+
configurationOptions.AsyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
33+
}
34+
35+
return configurationOptions;
36+
}
37+
}

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+
)]

src/OrchardCoreContrib.Garnet/Services/GarnetBus.cs

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using OrchardCore.Caching.Distributed;
44
using OrchardCore.Environment.Shell;
55
using StackExchange.Redis;
6-
using System.Diagnostics;
76
using System.Net;
87

98
namespace OrchardCoreContrib.Garnet.Services;
@@ -21,7 +20,7 @@ public class GarnetBus(
2120
ShellSettings shellSettings,
2221
ILogger<GarnetBus> logger) : IMessageBus
2322
{
24-
private readonly GarnetOptions _garnetOptions = garnetOptions.Value;
23+
private readonly ConfigurationOptions _configurationOptions = GarnetOptionsConverter.ConvertToConfigurationOptions(garnetOptions.Value);
2524
private readonly string _hostName = Dns.GetHostName() + ':' + Environment.ProcessId;
2625
private readonly string _channelPrefix = garnetService.InstancePrefix + shellSettings.Name + ':';
2726

@@ -40,7 +39,7 @@ public async Task SubscribeAsync(string channel, Action<string, string> handler)
4039
}
4140
}
4241

43-
var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(GetConfigurationOptions(_garnetOptions));
42+
var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(_configurationOptions);
4443

4544
try
4645
{
@@ -79,7 +78,7 @@ public async Task PublishAsync(string channel, string message)
7978
}
8079
}
8180

82-
var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(GetConfigurationOptions(_garnetOptions));
81+
var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(_configurationOptions);
8382

8483
try
8584
{
@@ -91,33 +90,4 @@ await connectionMultiplexer.GetSubscriber()
9190
logger.LogError(e, "Unable to publish to the channel '{ChannelName}'.", _channelPrefix + channel);
9291
}
9392
}
94-
95-
private static ConfigurationOptions GetConfigurationOptions(GarnetOptions garnetOptions)
96-
{
97-
var endPoints = new EndPointCollection
98-
{
99-
new DnsEndPoint(garnetOptions.Host, garnetOptions.Port)
100-
};
101-
var configOptions = new ConfigurationOptions
102-
{
103-
EndPoints = endPoints,
104-
ConnectTimeout = (int)TimeSpan.FromSeconds(2).TotalMilliseconds,
105-
SyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
106-
AsyncTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
107-
ReconnectRetryPolicy = new LinearRetry((int)TimeSpan.FromSeconds(10).TotalMilliseconds),
108-
ConnectRetry = 5,
109-
IncludeDetailInExceptions = true,
110-
AbortOnConnectFail = true,
111-
User = garnetOptions.UserName,
112-
Password = garnetOptions.Password
113-
};
114-
115-
if (Debugger.IsAttached)
116-
{
117-
configOptions.SyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
118-
configOptions.AsyncTimeout = (int)TimeSpan.FromHours(2).TotalMilliseconds;
119-
}
120-
121-
return configOptions;
122-
}
12393
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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.Net;
8+
9+
namespace OrchardCoreContrib.Garnet.Services;
10+
11+
/// <summary>
12+
/// Represents a distributed lock implementation based on Garnet service.
13+
/// </summary>
14+
/// <param name="garnetService">The <see cref="IGarnetService"/>.</param>
15+
/// <param name="garnetOptions">The <see cref="IOptions{GarnetOptions}"/>.</param>
16+
/// <param name="shellSettings">The <see cref="ShellSettings"/>.</param>
17+
/// <param name="logger">The <see cref="ILogger{GarnetLock}"/>.</param>
18+
public class GarnetLock(
19+
IGarnetService garnetService,
20+
IOptions<GarnetOptions> garnetOptions,
21+
ShellSettings shellSettings,
22+
ILogger<GarnetLock> logger) : IDistributedLock
23+
{
24+
private static readonly double _baseDelay = 100;
25+
private static readonly double _maxDelay = 10000;
26+
27+
private readonly string _hostName = Dns.GetHostName() + ':' + Environment.ProcessId;
28+
private readonly string _prefix = garnetService.InstancePrefix + shellSettings.Name + ':';
29+
private readonly ConfigurationOptions _configurationOptions = GarnetOptionsConverter.ConvertToConfigurationOptions(garnetOptions.Value);
30+
31+
/// <inheritdoc/>
32+
public async Task<ILocker> AcquireLockAsync(string key, TimeSpan? expiration = null)
33+
=> (await TryAcquireLockAsync(key, TimeSpan.MaxValue, expiration)).locker;
34+
35+
/// <inheritdoc/>
36+
public async Task<(ILocker locker, bool locked)> TryAcquireLockAsync(string key, TimeSpan timeout, TimeSpan? expiration = null)
37+
{
38+
using (var cts = new CancellationTokenSource(timeout != TimeSpan.MaxValue ? timeout : Timeout.InfiniteTimeSpan))
39+
{
40+
var retries = 0.0;
41+
42+
while (!cts.IsCancellationRequested)
43+
{
44+
var locked = await LockAsync(key, expiration ?? TimeSpan.MaxValue);
45+
46+
if (locked)
47+
{
48+
return (new Locker(this, key), locked);
49+
}
50+
51+
try
52+
{
53+
await Task.Delay(GetDelay(++retries), cts.Token);
54+
}
55+
catch (TaskCanceledException)
56+
{
57+
if (logger.IsEnabled(LogLevel.Debug))
58+
{
59+
logger.LogDebug("Timeout elapsed before acquiring the named lock '{LockName}' after the given timeout of '{Timeout}'.",
60+
_prefix + key, timeout.ToString());
61+
}
62+
}
63+
}
64+
}
65+
66+
return (null, false);
67+
}
68+
69+
/// <inheritdoc/>
70+
public async Task<bool> IsLockAcquiredAsync(string key)
71+
{
72+
if (garnetService.Client == null)
73+
{
74+
await garnetService.ConnectAsync();
75+
76+
if (garnetService.Client == null)
77+
{
78+
logger.LogError("Fails to check whether the named lock '{LockName}' is already acquired.", _prefix + key);
79+
80+
return false;
81+
}
82+
}
83+
84+
try
85+
{
86+
var database = (await ConnectionMultiplexer
87+
.ConnectAsync(_configurationOptions))
88+
.GetDatabase();
89+
90+
var lockValue = await database.LockQueryAsync(_prefix + key);
91+
92+
return lockValue.HasValue;
93+
}
94+
catch (Exception ex)
95+
{
96+
logger.LogError(ex, "Fails to check whether the named lock '{LockName}' is already acquired.", _prefix + key);
97+
}
98+
99+
return false;
100+
}
101+
102+
private async Task<bool> LockAsync(string key, TimeSpan expiry)
103+
{
104+
if (garnetService.Client is null)
105+
{
106+
await garnetService.ConnectAsync();
107+
108+
if (garnetService.Client is null)
109+
{
110+
logger.LogError("Fails to acquire the named lock '{LockName}'.", _prefix + key);
111+
112+
return false;
113+
}
114+
}
115+
116+
try
117+
{
118+
var database = (await ConnectionMultiplexer
119+
.ConnectAsync(_configurationOptions))
120+
.GetDatabase();
121+
122+
return await database.LockTakeAsync(_prefix + key, _hostName, expiry);
123+
}
124+
catch (Exception ex)
125+
{
126+
logger.LogError(ex, "Fails to acquire the named lock '{LockName}'.", _prefix + key);
127+
}
128+
129+
return false;
130+
}
131+
132+
private async ValueTask ReleaseAsync(string key)
133+
{
134+
try
135+
{
136+
var database = (await ConnectionMultiplexer
137+
.ConnectAsync(_configurationOptions))
138+
.GetDatabase();
139+
140+
await database.LockReleaseAsync(_prefix + key, _hostName);
141+
}
142+
catch (Exception ex)
143+
{
144+
logger.LogError(ex, "Fails to release the named lock '{LockName}'.", _prefix + key);
145+
}
146+
}
147+
148+
private void Release(string key)
149+
{
150+
try
151+
{
152+
var database = ConnectionMultiplexer
153+
.ConnectAsync(_configurationOptions)
154+
.GetAwaiter()
155+
.GetResult()
156+
.GetDatabase();
157+
158+
database.LockRelease(_prefix + key, _hostName);
159+
}
160+
catch (Exception ex)
161+
{
162+
logger.LogError(ex, "Fails to release the named lock '{LockName}'.", _prefix + key);
163+
}
164+
}
165+
166+
private sealed class Locker(GarnetLock garnetLock, string key) : ILocker
167+
{
168+
private bool _disposed;
169+
170+
public ValueTask DisposeAsync()
171+
{
172+
if (_disposed)
173+
{
174+
return default;
175+
}
176+
177+
_disposed = true;
178+
179+
return garnetLock.ReleaseAsync(key);
180+
}
181+
182+
public void Dispose()
183+
{
184+
if (_disposed)
185+
{
186+
return;
187+
}
188+
189+
_disposed = true;
190+
191+
garnetLock.Release(key);
192+
}
193+
}
194+
195+
private static TimeSpan GetDelay(double retries)
196+
{
197+
var delay = _baseDelay * (1.0 + ((Math.Pow(1.8, retries - 1.0) - 1.0) * (0.6 + new Random().NextDouble() * 0.4)));
198+
199+
return TimeSpan.FromMilliseconds(Math.Min(delay, _maxDelay));
200+
}
201+
}
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+
}

src/OrchardCoreContrib.Modules.Web/appsettings.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@
3838
"RequestUrlPrefix": "blog"
3939
}
4040
]
41-
},
42-
//"OrchardCoreContrib_Diagnostics_Elm": {
43-
// "Path": "/elm"
44-
//},
41+
}
4542
//"OrchardCoreContrib_HealthChecks": {
4643
// "Url": "/health",
4744
// "ShowDetails": true
@@ -50,7 +47,8 @@
5047
// "Host": "127.0.0.1",
5148
// "Port": 3278,
5249
// "Username": null,
53-
// "Password": null
50+
// "Password": null,
51+
// "InstancePrefix": ""
5452
//},
5553
//"OrchardCoreContrib_Gravatar": {
5654
// "Rating": 0,

0 commit comments

Comments
 (0)