Skip to content

Commit f6e67b2

Browse files
committed
refactor: Better and refactored RedisJobLock!
1 parent 5621f01 commit f6e67b2

File tree

4 files changed

+158
-106
lines changed

4 files changed

+158
-106
lines changed

CFLookup/Jobs/GetLatestUpdatedModPerGame.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ public async static Task RunAsync(PerformContext context)
2525

2626
var _db = _redis.GetDatabase(5);
2727

28-
using (var jobLock = new RedisJobLock(_redis, "GetLatestUpdatedModPerGame"))
28+
await using (var jobLock = await RedisJobLock.CreateAsync(
29+
_redis.GetDatabase(0),
30+
"GetLatestUpdatedModPerGame",
31+
scope.ServiceProvider.GetRequiredService<Logger<RedisJobLock>>(),
32+
TimeSpan.FromSeconds(15)))
2933
{
30-
if (!await jobLock.TryTakeLockAsync())
34+
if (jobLock == null)
3135
{
3236
return;
3337
}

CFLookup/Jobs/SaveMinecraftModStats.cs

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,67 +12,82 @@ namespace CFLookup.Jobs
1212
[AutomaticRetry(Attempts = 0)]
1313
public class SaveMinecraftModStats
1414
{
15-
public static async Task RunAsync(PerformContext context)
15+
public async static Task RunAsync(PerformContext context)
1616
{
1717
using (var scope = Program.ServiceProvider.CreateScope())
1818
{
1919
var cfClient = scope.ServiceProvider.GetRequiredService<ApiClient>();
2020
var db = scope.ServiceProvider.GetRequiredService<MSSQLDB>();
2121
var _redis = scope.ServiceProvider.GetRequiredService<ConnectionMultiplexer>();
2222

23-
var _db = _redis.GetDatabase(5);
23+
await using (var jobLock = await RedisJobLock.CreateAsync(
24+
_redis.GetDatabase(0),
25+
"SaveMinecraftModStats",
26+
scope.ServiceProvider.GetRequiredService<Logger<RedisJobLock>>(),
27+
TimeSpan.FromSeconds(15)))
28+
{
29+
if (jobLock == null)
30+
{
31+
return;
32+
}
2433

25-
var gameVersionTypes = await cfClient.GetGameVersionTypesAsync(432);
34+
var gameVersionTypes = await cfClient.GetGameVersionTypesAsync(432);
2635

27-
var minecraftVersions = gameVersionTypes.Data
28-
.Where(gvt => gvt.Slug.StartsWith("minecraft-") && !gvt.Slug.EndsWith("beta"))
29-
.OrderBy(gvt => Regex.Replace(gvt.Slug, "\\d+", m => m.Value.PadLeft(10, '0'))).ToList();
36+
var minecraftVersions = gameVersionTypes.Data
37+
.Where(gvt => gvt.Slug.StartsWith("minecraft-") && !gvt.Slug.EndsWith("beta"))
38+
.OrderBy(gvt => Regex.Replace(gvt.Slug, "\\d+", m => m.Value.PadLeft(10, '0'))).ToList();
3039

31-
var gameVersions = await cfClient.GetGameVersionsAsync(432);
40+
var gameVersions = await cfClient.GetGameVersionsAsync(432);
3241

33-
var filteredVersions = gameVersions.Data.Where(gv => minecraftVersions.Any(mv => mv.Id == gv.Type))
34-
.ToDictionary(gv => gv.Type, gv => gv.Versions.OrderBy(gvt => Regex.Replace(gvt, "\\d+", m => m.Value.PadLeft(10, '0'))));
42+
var filteredVersions = gameVersions.Data.Where(gv => minecraftVersions.Any(mv => mv.Id == gv.Type))
43+
.ToDictionary(gv => gv.Type,
44+
gv => gv.Versions.OrderBy(gvt =>
45+
Regex.Replace(gvt, "\\d+", m => m.Value.PadLeft(10, '0'))));
3546

36-
var mvList = new List<MinecraftVersionHolder>();
47+
var mvList = new List<MinecraftVersionHolder>();
3748

38-
foreach (var minecraftVersion in minecraftVersions)
39-
{
40-
var holder = new MinecraftVersionHolder
49+
foreach (var minecraftVersion in minecraftVersions)
4150
{
42-
VersionId = minecraftVersion.Id,
43-
GameVersion = minecraftVersion.Name,
44-
GameSubVersions = filteredVersions[minecraftVersion.Id].ToList()
45-
};
51+
var holder = new MinecraftVersionHolder
52+
{
53+
VersionId = minecraftVersion.Id,
54+
GameVersion = minecraftVersion.Name,
55+
GameSubVersions = filteredVersions[minecraftVersion.Id].ToList()
56+
};
4657

47-
mvList.Add(holder);
48-
}
58+
mvList.Add(holder);
59+
}
4960

50-
var modLoaders = new Dictionary<string, ModLoaderType>
51-
{
52-
{ "Forge", ModLoaderType.Forge },
53-
{ "Fabric", ModLoaderType.Fabric },
54-
{ "Quilt", ModLoaderType.Quilt },
55-
{ "NeoForge", ModLoaderType.NeoForge }
56-
};
61+
var modLoaders = new Dictionary<string, ModLoaderType>
62+
{
63+
{ "Forge", ModLoaderType.Forge },
64+
{ "Fabric", ModLoaderType.Fabric },
65+
{ "Quilt", ModLoaderType.Quilt },
66+
{ "NeoForge", ModLoaderType.NeoForge }
67+
};
5768

58-
var modsPerVersion = new Dictionary<string, Dictionary<string, long>>();
69+
var modsPerVersion = new Dictionary<string, Dictionary<string, long>>();
5970

60-
foreach (var gameVersion in mvList)
61-
{
62-
foreach (var subversion in gameVersion.GameSubVersions)
71+
foreach (var gameVersion in mvList)
6372
{
64-
modsPerVersion.Add(subversion, new Dictionary<string, long>());
65-
foreach(var modloader in modLoaders)
66-
{
67-
var mods = await cfClient.SearchModsAsync(432, 6, gameVersion: subversion, modLoaderType: modloader.Value, pageSize: 1);
68-
modsPerVersion[subversion].Add(modloader.Key, mods.Pagination.TotalCount);
73+
foreach (var subversion in gameVersion.GameSubVersions)
74+
{
75+
modsPerVersion.Add(subversion, new Dictionary<string, long>());
76+
foreach (var modloader in modLoaders)
77+
{
78+
var mods = await cfClient.SearchModsAsync(432, 6, gameVersion: subversion,
79+
modLoaderType: modloader.Value, pageSize: 1);
80+
modsPerVersion[subversion].Add(modloader.Key, mods.Pagination.TotalCount);
81+
}
6982
}
7083
}
71-
}
7284

73-
var json = JsonSerializer.Serialize(modsPerVersion);
85+
var json = JsonSerializer.Serialize(modsPerVersion);
7486

75-
await db.ExecuteNonQueryAsync("INSERT INTO [dbo].[MinecraftModStatsOverTime] ([stats]) VALUES (@stats)", new SqlParameter("@stats", json));
87+
await db.ExecuteNonQueryAsync(
88+
"INSERT INTO [dbo].[MinecraftModStatsOverTime] ([stats]) VALUES (@stats)",
89+
new SqlParameter("@stats", json));
90+
}
7691
}
7792
}
7893

@@ -83,4 +98,4 @@ public class MinecraftVersionHolder
8398
public List<string> GameSubVersions { get; set; } = new List<string>();
8499
}
85100
}
86-
}
101+
}

CFLookup/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class Program
2020
private static async Task Main(string[] args)
2121
{
2222
var builder = WebApplication.CreateBuilder(args);
23+
24+
builder.Services.AddLogging(_builder => _builder.AddConsole());
2325

2426
builder.Services.AddResponseCompression(options =>
2527
{

CFLookup/RedisJobLock.cs

Lines changed: 96 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,125 @@
22

33
namespace CFLookup
44
{
5-
public class RedisJobLock : IDisposable
5+
public sealed class RedisJobLock : IAsyncDisposable
66
{
7-
private readonly ConnectionMultiplexer _connectionMultiplexer;
8-
private string TypeName { get; set; }
9-
private string Handle { get; set; }
7+
private readonly ILogger<RedisJobLock> _logger;
8+
private readonly TimeSpan _expiryTime;
9+
private string _lockKey { get; }
1010

11-
private IDatabase RDB { get { return _connectionMultiplexer.GetDatabase(0); } }
12-
private Guid LockInstance;
13-
private CancellationToken _cancellationToken;
14-
private readonly CancellationTokenSource CTS;
11+
private IDatabase _redisDatabase { get; }
12+
private readonly string _lockOwner;
13+
private readonly CancellationTokenSource _cts;
14+
private Task? _refreshLockTask;
1515

16-
public RedisJobLock(ConnectionMultiplexer connectionMultiplexer, string lockName)
16+
private RedisJobLock(IDatabase database, string lockName, ILogger<RedisJobLock> logger, TimeSpan expiryTime)
1717
{
18-
_connectionMultiplexer = connectionMultiplexer;
19-
CTS = new CancellationTokenSource();
18+
_logger = logger;
19+
_expiryTime = expiryTime;
20+
_redisDatabase = database;
2021

21-
TypeName = lockName;
22-
LockInstance = Guid.NewGuid();
23-
_cancellationToken = CTS.Token;
22+
_lockOwner = Guid.NewGuid().ToString();
2423

25-
Handle = GetHandle(TypeName);
24+
_lockKey = $"joblock:{lockName}";
25+
_cts = new CancellationTokenSource();
2626
}
2727

28-
public bool IsLocked(string lockName)
28+
public async static Task<RedisJobLock?> CreateAsync(
29+
IDatabase database,
30+
string lockName,
31+
ILogger<RedisJobLock> logger,
32+
TimeSpan expiryTime
33+
)
2934
{
30-
var handle = GetHandle(lockName);
31-
return RDB.LockQuery(handle) != RedisValue.Null;
32-
}
33-
34-
private static string GetHandle(string lockName)
35-
{
36-
return $"joblock:{lockName}";
37-
}
35+
var distributedLock = new RedisJobLock(database, lockName, logger, expiryTime);
3836

39-
TimeSpan LockTime = TimeSpan.FromSeconds(15);
37+
var lockAcquired = await database.LockTakeAsync(
38+
distributedLock._lockKey,
39+
distributedLock._lockOwner,
40+
distributedLock._expiryTime);
4041

41-
Task _refreshLockTask;
42-
async Task RefreshLockAsync()
43-
{
44-
try
45-
{
46-
await Task.Delay(LockTime.Subtract(TimeSpan.FromMilliseconds(LockTime.TotalMilliseconds / 2)), _cancellationToken);
47-
KeepLocked();
48-
_refreshLockTask = RefreshLockAsync();
49-
_ = PubSubLog($"Refreshing lock {Handle} / {LockInstance}");
50-
}
51-
catch (TaskCanceledException)
42+
if (lockAcquired)
5243
{
53-
/* This is totally ok, we cancelled it */
44+
distributedLock.StartRenewalTask();
45+
logger.LogDebug("Lock acquired for key {LockKey} by owner {LockOwner}", distributedLock._lockKey, distributedLock._lockOwner);
46+
return distributedLock;
5447
}
48+
49+
logger.LogDebug("Failed to acquire lock for key {LockKey}", distributedLock._lockKey);
50+
return null;
5551
}
5652

57-
private void KeepLocked()
53+
private void StartRenewalTask()
5854
{
59-
RDB.LockExtend(Handle, LockInstance.ToString(), LockTime);
60-
}
55+
_refreshLockTask = Task.Run(async () =>
56+
{
57+
var renewalDelay = TimeSpan.FromMilliseconds(_expiryTime.TotalMilliseconds / 2.5);
6158

62-
public async Task KeepLockedAsync()
63-
{
64-
await RDB.LockExtendAsync(Handle, LockInstance.ToString(), LockTime);
65-
}
59+
while (!_cts.IsCancellationRequested)
60+
{
61+
try
62+
{
63+
await Task.Delay(renewalDelay, _cts.Token);
6664

67-
public bool TryTakeLock()
68-
{
69-
var couldTakeLock = RDB.LockTake(Handle, LockInstance.ToString(), LockTime);
70-
if (couldTakeLock) _refreshLockTask = RefreshLockAsync();
71-
_ = PubSubLog($"Could {(couldTakeLock ? "" : "not ")}take lock for {Handle} / {LockInstance}");
72-
return couldTakeLock;
73-
}
65+
var renewed = await _redisDatabase.LockExtendAsync(_lockKey, _lockOwner, _expiryTime);
7466

75-
public async Task<bool> TryTakeLockAsync()
76-
{
77-
var couldTakeLock = await RDB.LockTakeAsync(Handle, LockInstance.ToString(), LockTime);
78-
if (couldTakeLock) _refreshLockTask = RefreshLockAsync();
79-
_ = PubSubLog($"Could {(couldTakeLock ? "" : "not ")}take lock for {Handle} / {LockInstance}");
80-
return couldTakeLock;
67+
if (renewed)
68+
{
69+
_logger.LogDebug("Renewed lock for key {LockKey}", _lockKey);
70+
}
71+
else
72+
{
73+
_logger.LogError("Failed to renew lock for key {LockKey}. Lock has been lost.", _lockKey);
74+
break;
75+
}
76+
}
77+
catch (TaskCanceledException)
78+
{
79+
break;
80+
}
81+
catch (Exception ex)
82+
{
83+
_logger.LogError(ex, "Failed to renew lock for key {LockKey} due to exception.", _lockKey);
84+
break;
85+
}
86+
}
87+
});
8188
}
8289

83-
internal async Task PubSubLog(string logmessage)
90+
public async ValueTask DisposeAsync()
8491
{
85-
await _connectionMultiplexer.GetSubscriber().PublishAsync("LockMessages/CFLookup", logmessage);
86-
}
92+
if (_refreshLockTask == null)
93+
{
94+
return;
95+
}
96+
97+
_logger.LogDebug($"Releasing lock {_lockKey} / {_lockOwner}");
8798

88-
public void Dispose()
89-
{
90-
_ = PubSubLog($"Releasing lock {Handle} / {LockInstance}");
91-
RDB.LockRelease(Handle, LockInstance.ToString());
92-
CTS.Cancel();
99+
if (!_cts.IsCancellationRequested)
100+
{
101+
await _cts.CancelAsync();
102+
}
103+
104+
var released = await _redisDatabase.LockReleaseAsync(_lockKey, _lockOwner);
105+
if (!released)
106+
{
107+
_logger.LogWarning("Failed to release lock for key {LockKey}, it may have expired already", _lockKey);
108+
}
109+
else
110+
{
111+
_logger.LogDebug("Released lock for key {LockKey}", _lockKey);
112+
}
113+
114+
try
115+
{
116+
await _refreshLockTask;
117+
}
118+
catch (Exception ex)
119+
{
120+
_logger.LogError(ex, "Failed to refresh lock for key {LockKey}", _lockKey);
121+
}
122+
123+
_cts.Dispose();
93124
}
94125
}
95126
}

0 commit comments

Comments
 (0)