Skip to content

Commit 99382e0

Browse files
Merge pull request #68 from nullinside-development-group/feature/lock
feat: using database lock to protect bot api token
2 parents 831505b + 17c9dc0 commit 99382e0

File tree

2 files changed

+72
-10
lines changed

2 files changed

+72
-10
lines changed

src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
using Microsoft.EntityFrameworkCore;
1+
using System.Diagnostics;
22

3+
using log4net;
4+
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.EntityFrameworkCore.Storage;
7+
8+
using Nullinside.Api.Common;
39
using Nullinside.Api.Common.Twitch;
410
using Nullinside.Api.Model;
511
using Nullinside.Api.Model.Ddl;
@@ -10,14 +16,24 @@ namespace Nullinside.Api.TwitchBot.Model;
1016
/// Extensions for <see cref="NullinsideContext" /> and its ERMs.
1117
/// </summary>
1218
public static class NullinsideContextExtensions {
19+
/// <summary>
20+
/// The database lock name to use as a lock in the MySQL database.
21+
/// </summary>
22+
private const string BOT_REFRESH_TOKEN_LOCK_NAME = "bot_refresh_token";
23+
24+
/// <summary>
25+
/// The logger.
26+
/// </summary>
27+
private static readonly ILog _log = LogManager.GetLogger(typeof(NullinsideContextExtensions));
28+
1329
/// <summary>
1430
/// Gets a twitch api proxy.
1531
/// </summary>
1632
/// <param name="user">The user to configure the proxy as.</param>
1733
/// <param name="api">The twitch api object currently in use.</param>
1834
/// <returns>The twitch api.</returns>
1935
public static void Configure(this ITwitchApiProxy api, User user) {
20-
api.OAuth = new() {
36+
api.OAuth = new TwitchAccessToken {
2137
AccessToken = user.TwitchToken,
2238
RefreshToken = user.TwitchRefreshToken,
2339
ExpiresUtc = user.TwitchTokenExpiration
@@ -36,17 +52,63 @@ public static void Configure(this ITwitchApiProxy api, User user) {
3652
ITwitchApiProxy api, CancellationToken stoppingToken = new()) {
3753
api.Configure(user);
3854

39-
// Refresh its token if necessary.
55+
// Use the token we have, if it hasn't expired.
4056
if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > user.TwitchTokenExpiration)) {
4157
return api;
4258
}
4359

44-
if (null == await api.RefreshAccessToken(stoppingToken) || null == api.OAuth) {
45-
return api;
46-
}
60+
// Database locking requires ExecutionStrategy
61+
IExecutionStrategy strat = db.Database.CreateExecutionStrategy();
62+
return await strat.ExecuteAsync(async () => {
63+
bool failed = false;
64+
65+
// Database locking requires transaction scope
66+
await using IDbContextTransaction scope = await db.Database.BeginTransactionAsync(stoppingToken);
67+
68+
// Perform the database lock
69+
using var dbLock = new DatabaseLock(db);
70+
var sw = new Stopwatch();
71+
sw.Start();
72+
await dbLock.GetLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken);
73+
_log.Info($"bot_refresh_token: {sw.Elapsed}");
74+
sw.Stop();
75+
76+
try {
77+
// Get the user with the database lock acquired.
78+
User? updatedUser = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == user.Id, stoppingToken);
79+
if (null == updatedUser) {
80+
return null;
81+
}
82+
83+
// Use the token we have, if it hasn't expired.
84+
if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > updatedUser.TwitchTokenExpiration)) {
85+
api.Configure(updatedUser);
86+
return api;
87+
}
88+
89+
// Refresh the token with the Twitch API.
90+
TwitchAccessToken? newToken = await api.RefreshAccessToken(stoppingToken);
91+
if (null == newToken) {
92+
return null;
93+
}
94+
95+
// Update the credentials in the database.
96+
await db.UpdateOAuthInDatabase(user.Id, newToken, stoppingToken);
97+
return api;
98+
}
99+
catch {
100+
failed = true;
101+
}
102+
finally {
103+
await dbLock.ReleaseLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken);
104+
105+
if (!failed) {
106+
scope.Commit();
107+
}
108+
}
47109

48-
await db.UpdateOAuthInDatabase(user.Id, api.OAuth, stoppingToken);
49-
return api;
110+
return null;
111+
});
50112
}
51113

52114
/// <summary>
@@ -57,7 +119,7 @@ public static void Configure(this ITwitchApiProxy api, User user) {
57119
/// <param name="oAuth">The OAuth information.</param>
58120
/// <param name="stoppingToken">The stopping token.</param>
59121
/// <returns>The number of state entries written to the database.</returns>
60-
public static async Task<int> UpdateOAuthInDatabase(this INullinsideContext db, int userId,
122+
private static async Task<int> UpdateOAuthInDatabase(this INullinsideContext db, int userId,
61123
TwitchAccessToken oAuth, CancellationToken stoppingToken = new()) {
62124
User? row = await db.Users.FirstOrDefaultAsync(u => u.Id == userId, stoppingToken);
63125
if (null == row) {

0 commit comments

Comments
 (0)