diff --git a/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs b/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs index 02571f7..70f5f33 100644 --- a/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs +++ b/src/Nullinside.Api.TwitchBot/Model/NullinsideContextExtensions.cs @@ -1,5 +1,11 @@ -using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using log4net; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +using Nullinside.Api.Common; using Nullinside.Api.Common.Twitch; using Nullinside.Api.Model; using Nullinside.Api.Model.Ddl; @@ -10,6 +16,16 @@ namespace Nullinside.Api.TwitchBot.Model; /// Extensions for and its ERMs. /// public static class NullinsideContextExtensions { + /// + /// The database lock name to use as a lock in the MySQL database. + /// + private const string BOT_REFRESH_TOKEN_LOCK_NAME = "bot_refresh_token"; + + /// + /// The logger. + /// + private static readonly ILog _log = LogManager.GetLogger(typeof(NullinsideContextExtensions)); + /// /// Gets a twitch api proxy. /// @@ -17,7 +33,7 @@ public static class NullinsideContextExtensions { /// The twitch api object currently in use. /// The twitch api. public static void Configure(this ITwitchApiProxy api, User user) { - api.OAuth = new() { + api.OAuth = new TwitchAccessToken { AccessToken = user.TwitchToken, RefreshToken = user.TwitchRefreshToken, ExpiresUtc = user.TwitchTokenExpiration @@ -36,17 +52,63 @@ public static void Configure(this ITwitchApiProxy api, User user) { ITwitchApiProxy api, CancellationToken stoppingToken = new()) { api.Configure(user); - // Refresh its token if necessary. + // Use the token we have, if it hasn't expired. if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > user.TwitchTokenExpiration)) { return api; } - if (null == await api.RefreshAccessToken(stoppingToken) || null == api.OAuth) { - return api; - } + // Database locking requires ExecutionStrategy + IExecutionStrategy strat = db.Database.CreateExecutionStrategy(); + return await strat.ExecuteAsync(async () => { + bool failed = false; + + // Database locking requires transaction scope + await using IDbContextTransaction scope = await db.Database.BeginTransactionAsync(stoppingToken); + + // Perform the database lock + using var dbLock = new DatabaseLock(db); + var sw = new Stopwatch(); + sw.Start(); + await dbLock.GetLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken); + _log.Info($"bot_refresh_token: {sw.Elapsed}"); + sw.Stop(); + + try { + // Get the user with the database lock acquired. + User? updatedUser = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == user.Id, stoppingToken); + if (null == updatedUser) { + return null; + } + + // Use the token we have, if it hasn't expired. + if (!(DateTime.UtcNow + TimeSpan.FromHours(1) > updatedUser.TwitchTokenExpiration)) { + api.Configure(updatedUser); + return api; + } + + // Refresh the token with the Twitch API. + TwitchAccessToken? newToken = await api.RefreshAccessToken(stoppingToken); + if (null == newToken) { + return null; + } + + // Update the credentials in the database. + await db.UpdateOAuthInDatabase(user.Id, newToken, stoppingToken); + return api; + } + catch { + failed = true; + } + finally { + await dbLock.ReleaseLock(BOT_REFRESH_TOKEN_LOCK_NAME, stoppingToken); + + if (!failed) { + scope.Commit(); + } + } - await db.UpdateOAuthInDatabase(user.Id, api.OAuth, stoppingToken); - return api; + return null; + }); } /// @@ -57,7 +119,7 @@ public static void Configure(this ITwitchApiProxy api, User user) { /// The OAuth information. /// The stopping token. /// The number of state entries written to the database. - public static async Task UpdateOAuthInDatabase(this INullinsideContext db, int userId, + private static async Task UpdateOAuthInDatabase(this INullinsideContext db, int userId, TwitchAccessToken oAuth, CancellationToken stoppingToken = new()) { User? row = await db.Users.FirstOrDefaultAsync(u => u.Id == userId, stoppingToken); if (null == row) { diff --git a/src/nullinside-api b/src/nullinside-api index 55f80e6..aa08d21 160000 --- a/src/nullinside-api +++ b/src/nullinside-api @@ -1 +1 @@ -Subproject commit 55f80e6b981e8f39c991d0cb44112678d99d64d8 +Subproject commit aa08d219a5cc12fbef0c1a420aee92cde549c7a8